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::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    /// Link the objects into an executable or a shared library.
33    Link,
34}
35
36impl Phase {
37    /// The name used in `-###` output and in diagnostics.
38    #[must_use]
39    pub fn as_str(self) -> &'static str {
40        match self {
41            Phase::Preprocess => "preprocess",
42            Phase::Compile => "compile",
43            Phase::Assemble => "assemble",
44            Phase::Link => "link",
45        }
46    }
47}
48
49impl std::fmt::Display for Phase {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.write_str(self.as_str())
52    }
53}
54
55/// What an input file is, which decides where in the pipeline it enters.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum InputKind {
58    /// C source. Extension `.c`, or `-x c`.
59    C,
60    /// A header compiled on its own. Extension `.h` with `-x c-header`, or `-x c-header`.
61    CHeader,
62    /// Already preprocessed C. Extension `.i`, or `-x cpp-output`.
63    PreprocessedC,
64    /// The IR this compiler prints. Extension `.ir`, or `-x ir`.
65    ///
66    /// Not a GCC input kind, because GCC has no textual IR. It is here because the IR's
67    /// printer and its parser are a pair, and a pair is only known to agree if something reads
68    /// back what was written: `rucc --emit=ir a.c -o a.ir` and then `rucc --emit=ir a.ir` are
69    /// two files a byte comparison has an opinion about, over whatever code is at hand rather
70    /// than over the modules a test happens to build.
71    Ir,
72    /// Assembly. Extension `.s`, or `-x assembler`.
73    Assembler,
74    /// Assembly that still needs the preprocessor. Extension `.S` or `.sx`, or
75    /// `-x assembler-with-cpp`.
76    AssemblerWithCpp,
77    /// An object file, an archive or a shared library. Anything the linker takes directly.
78    LinkerInput,
79}
80
81impl InputKind {
82    /// The name `-x` uses for this kind, where one exists.
83    #[must_use]
84    pub fn as_str(self) -> &'static str {
85        match self {
86            InputKind::C => "c",
87            InputKind::CHeader => "c-header",
88            InputKind::PreprocessedC => "cpp-output",
89            InputKind::Ir => "ir",
90            InputKind::Assembler => "assembler",
91            InputKind::AssemblerWithCpp => "assembler-with-cpp",
92            InputKind::LinkerInput => "linker-input",
93        }
94    }
95
96    /// Parses the argument of `-x`.
97    ///
98    /// # Errors
99    ///
100    /// Returns the offending name when it is not one we accept. C++ gets its own message,
101    /// because "unknown language c++" reads like an oversight and it is a decision.
102    pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
103        match name {
104            "c" => Ok(InputKind::C),
105            "c-header" => Ok(InputKind::CHeader),
106            "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
107            "ir" => Ok(InputKind::Ir),
108            "assembler" => Ok(InputKind::Assembler),
109            "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
110            "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
111                Err(XError::Unsupported(name.to_owned()))
112            }
113            _ => Err(XError::Unknown(name.to_owned())),
114        }
115    }
116
117    /// Classifies an input by its extension, the way `spec/04-driver-and-cli.md` section 4.2
118    /// tabulates it.
119    ///
120    /// An unrecognized extension is a linker input, which is GCC's behavior and is what makes
121    /// `rucc foo.o bar.builtin-suffix` work. The exception is a C++ extension, which is a
122    /// hard error rather than a confusing link failure later.
123    ///
124    /// # Errors
125    ///
126    /// Returns the extension when it names a language that is permanently out of scope.
127    pub fn from_path(path: &str) -> Result<InputKind, XError> {
128        let ext = extension(path);
129        match ext {
130            // Matched case-sensitively on purpose: `.S` and `.s` are different languages and
131            // conflating them is a real bug on case-insensitive file systems that GCC also
132            // has. The comment is here so the next person does not "fix" it.
133            "c" => Ok(InputKind::C),
134            "i" => Ok(InputKind::PreprocessedC),
135            "ir" => Ok(InputKind::Ir),
136            "h" => Ok(InputKind::CHeader),
137            "s" => Ok(InputKind::Assembler),
138            "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
139            "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
140                Err(XError::Unsupported(ext.to_owned()))
141            }
142            _ => Ok(InputKind::LinkerInput),
143        }
144    }
145
146    /// The full phase sequence for this kind, before any mode flag truncates it.
147    fn full_sequence(self) -> &'static [Phase] {
148        use Phase::{Assemble, Compile, Link, Preprocess};
149        match self {
150            InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
151            InputKind::PreprocessedC | InputKind::Ir => &[Compile, Assemble, Link],
152            // Note the gap: assembly with a preprocessor skips `Compile` entirely. This is why
153            // the sequence is a list rather than a range over the enum.
154            InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
155            InputKind::Assembler => &[Assemble, Link],
156            InputKind::LinkerInput => &[Link],
157        }
158    }
159}
160
161/// Why an input or an `-x` argument was rejected.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum XError {
164    /// A language we do not know at all.
165    Unknown(String),
166    /// A language we know and will not implement.
167    Unsupported(String),
168}
169
170impl std::fmt::Display for XError {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            XError::Unknown(name) => {
174                write!(
175                    f,
176                    "unknown language `{name}`; \
177                     accepted: c, c-header, cpp-output, ir, assembler, assembler-with-cpp, none"
178                )
179            }
180            XError::Unsupported(name) => {
181                write!(
182                    f,
183                    "`{name}` is not C, and this compiler is only ever going to compile C; \
184                     see the not-in-scope list in spec/00-README.md"
185                )
186            }
187        }
188    }
189}
190
191impl std::error::Error for XError {}
192
193/// One input file, with the `-x` setting that was in effect where it appeared.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Input {
196    /// The path as it was written on the command line, or the name of a `-l` library.
197    pub path: String,
198    /// The language forced by an earlier `-x`, if any. `-x none` clears it.
199    pub forced: Option<InputKind>,
200    /// Whether this came from `-l<name>` rather than being a path.
201    ///
202    /// A library is an input to the link and is held here rather than beside the other link
203    /// flags, because where it falls among the objects is what decides whether it is searched
204    /// for what they left undefined. A list of objects and a separate list of libraries would
205    /// lose exactly that.
206    pub library: bool,
207}
208
209impl Input {
210    /// An input with no `-x` in effect.
211    #[must_use]
212    pub fn new(path: impl Into<String>) -> Input {
213        Input { path: path.into(), forced: None, library: false }
214    }
215
216    /// `-l<name>`, which is an input to the link and to nothing else.
217    #[must_use]
218    pub fn library(name: impl Into<String>) -> Input {
219        Input { path: name.into(), forced: None, library: true }
220    }
221
222    /// What this input is, taking `-x` into account.
223    ///
224    /// # Errors
225    ///
226    /// Returns the extension when it names a language that is out of scope.
227    pub fn kind(&self) -> Result<InputKind, XError> {
228        if self.library {
229            return Ok(InputKind::LinkerInput);
230        }
231        match self.forced {
232            Some(k) => Ok(k),
233            None => InputKind::from_path(&self.path),
234        }
235    }
236}
237
238/// Where the result of a job goes.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum Output {
241    /// Standard output, which is where `-E` writes when there is no `-o`.
242    Stdout,
243    /// A path the user can see and named, or that we derived from the input name.
244    File(String),
245    /// A file the link step consumes and nothing else ever sees. The name is a hint for
246    /// `-###` output; the real path is chosen in a temporary directory at execution time.
247    Temporary(String),
248}
249
250impl Output {
251    fn render(&self) -> String {
252        match self {
253            Output::Stdout => "-".to_owned(),
254            Output::File(p) => p.clone(),
255            Output::Temporary(p) => format!("{p} (temporary)"),
256        }
257    }
258
259    /// The path the link step reads, for an output that feeds it.
260    fn as_link_input(&self) -> Option<&str> {
261        match self {
262            Output::File(p) | Output::Temporary(p) => Some(p),
263            Output::Stdout => None,
264        }
265    }
266}
267
268/// Everything that has to happen to one input file.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Job {
271    /// The input path as written.
272    pub input: String,
273    /// What we decided it is.
274    pub kind: InputKind,
275    /// The phases to run, in order. Empty when the input goes straight to the linker.
276    pub phases: Vec<Phase>,
277    /// Where the last phase writes.
278    pub output: Output,
279    /// What the files `-save-temps` keeps are called, without the suffix that says which one it
280    /// is, or `None` when there is nothing to keep.
281    ///
282    /// Nothing to keep is the usual case: the flag was not given, or it was and this job has no
283    /// step whose result the compilation would have thrown away. `-E -save-temps` is the second
284    /// of those, since the preprocessed text is the output and is already being written.
285    pub aux_base: Option<String>,
286}
287
288impl Job {
289    /// Where the preprocessed text goes when `-save-temps` asked for it to be kept.
290    ///
291    /// `None` when the flag was not given, when the input arrives preprocessed already and there
292    /// is no phase 4 to keep the result of, or when the text is this job's own output and is
293    /// being written anyway.
294    #[must_use]
295    pub fn saved_text(&self) -> Option<String> {
296        let base = self.aux_base.as_ref()?;
297        self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
298    }
299
300    /// Where the assembly goes when `-save-temps` asked for it to be kept.
301    ///
302    /// `None` for the same reasons, the last of them being `-S`: the assembly is the output
303    /// there, and a copy of it under a second name is a file nobody asked for.
304    #[must_use]
305    pub fn saved_asm(&self) -> Option<String> {
306        let base = self.aux_base.as_ref()?;
307        let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
308        (past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
309    }
310}
311
312/// The link step, when there is one.
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct LinkJob {
315    /// Objects and libraries, in command line order, because link order is semantic.
316    pub inputs: Vec<Item>,
317    /// The executable.
318    pub output: String,
319}
320
321/// The whole plan for one invocation.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct Plan {
324    /// One per input, in command line order.
325    pub jobs: Vec<Job>,
326    /// The link step, or `None` when a mode flag stopped short of it.
327    pub link: Option<LinkJob>,
328    /// Things worth saying under `-v` that are not errors, such as an object file passed on a
329    /// command line that is not linking.
330    pub notes: Vec<String>,
331    /// The argument of `-o` as it was written, if it was given.
332    ///
333    /// Kept alongside the paths it produced because the `-M` family needs the name rather than
334    /// the path: a make rule whose target is the object the build asked for is one the build
335    /// can read back, and a rule naming a temporary directory is one nothing will ever match.
336    pub output: Option<String>,
337}
338
339/// Why a command line could not be turned into a plan.
340#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct PlanError {
342    /// Lowercase, no trailing period, the same shape as every other diagnostic.
343    pub message: String,
344}
345
346impl std::fmt::Display for PlanError {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        f.write_str(&self.message)
349    }
350}
351
352impl std::error::Error for PlanError {}
353
354fn plan_err(message: impl Into<String>) -> PlanError {
355    PlanError { message: message.into() }
356}
357
358/// The last phase that runs, given what the user asked to be emitted.
359///
360/// `--emit=tast` and the other intermediate dumps stop where `-S` stops, because they are
361/// produced inside the compile phase and there is nothing after them to run.
362#[must_use]
363pub fn last_phase(emit: EmitKind) -> Phase {
364    match emit {
365        EmitKind::Preprocessed => Phase::Preprocess,
366        EmitKind::Asm
367        | EmitKind::Tast
368        | EmitKind::Ir
369        | EmitKind::MirFinal
370        | EmitKind::SafetySummary
371        | EmitKind::TypeGranules => Phase::Compile,
372        EmitKind::Object => Phase::Assemble,
373        EmitKind::Executable => Phase::Link,
374    }
375}
376
377/// The extension of a path, without the dot, or the empty string when there is none.
378fn extension(path: &str) -> &str {
379    let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
380    match name.rfind('.') {
381        // A leading dot is a hidden file, not an extension, and `.` and `..` are not inputs.
382        Some(0) | None => "",
383        Some(i) => &name[i + 1..],
384    }
385}
386
387/// The last component of a path, which is the whole of it when there is no directory in it.
388fn file_part(path: &str) -> &str {
389    path.rsplit(['/', '\\']).next().unwrap_or(path)
390}
391
392/// The path with its extension taken off and its directory left on.
393///
394/// This is what a name derived from `-o` is built on, since `-o out/a.o` puts the files that go
395/// beside the object in `out` and not in the working directory.
396fn without_extension(path: &str) -> &str {
397    let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
398    match path[start..].rfind('.') {
399        // A leading dot is a hidden file, not an extension, and `.` and `..` are not inputs.
400        Some(0) | None => path,
401        Some(i) => &path[..start + i],
402    }
403}
404
405/// The path without its extension, keeping any directory part off, because GCC writes the
406/// output into the current directory rather than next to the source.
407fn stem(path: &str) -> &str {
408    file_part(without_extension(path))
409}
410
411/// The name the files `-save-temps` keeps are built from, without the suffix that says which
412/// one it is.
413///
414/// GCC calls this the auxiliary base name, and it is the name of the file the compilation
415/// produces with the extension taken off: `-c a.c -o out/a.o` keeps `out/a.i` and `out/a.s`. A
416/// command line that links has one output for however many inputs, so the input's own name goes
417/// on the end and `a.c` under `-o out/prog` becomes `out/prog-a`. `-save-temps=cwd` is the same
418/// name with the directory taken off, which is the only thing the two spellings disagree about.
419fn aux_base(opts: &Options, input: &str, output: Option<&str>, linking: bool) -> String {
420    let named = match output {
421        Some(o) => without_extension(o),
422        // No `-o`, so the job worked its own name out, and a worked out name has no directory in
423        // it: the object of `sub/a.c` is `a.o` in the working directory, so what is kept beside
424        // it is in the working directory too.
425        None if linking => stem(default_exe(opts)),
426        None => stem(input),
427    };
428    let named = if opts.save_temps == SaveTemps::Cwd { file_part(named) } else { named };
429    if linking { format!("{named}-{}", stem(input)) } else { named.to_owned() }
430}
431
432/// The suffix a phase's output carries, for this target.
433fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
434    match phase {
435        Phase::Preprocess => "i",
436        // The compile phase is where every intermediate dump comes out, and each of them is a
437        // different language, so each gets a name of its own. `rucc --emit=tast a.c` writing
438        // `a.s` would be a file that neither an assembler nor a reader could make sense of.
439        Phase::Compile => match opts.emit {
440            EmitKind::Tast => "tast",
441            EmitKind::Ir => "ir",
442            EmitKind::MirFinal => "mir",
443            // Two extensions rather than one, because the content is JSON and a tool that reads
444            // JSON should be able to tell by looking, and because `a.json` next to `a.c` says
445            // nothing about which of a build's several JSON files it is.
446            EmitKind::SafetySummary => "safety.json",
447            // Two extensions for the same reason, and text rather than JSON because this one
448            // is read by a person once and not by a build every time.
449            EmitKind::TypeGranules => "granules.txt",
450            _ => "s",
451        },
452        // MSVC-targeted builds expect `.obj`, and build systems written for that target look
453        // for it by name.
454        Phase::Assemble => {
455            if opts.target.os == Os::Windows {
456                "obj"
457            } else {
458                "o"
459            }
460        }
461        Phase::Link => "",
462    }
463}
464
465/// The default name of the linked output, which is GCC's `a.out` everywhere but Windows.
466fn default_exe(opts: &Options) -> &'static str {
467    if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
468}
469
470impl Plan {
471    /// Builds the plan for one invocation.
472    ///
473    /// `output` is the argument of `-o`, if it was given.
474    ///
475    /// # Errors
476    ///
477    /// Returns a message when the inputs and the mode flags do not describe a compilation:
478    /// an out of scope language, `-o` naming one file for several outputs, or nothing to do.
479    pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
480        if inputs.is_empty() {
481            return Err(plan_err("no input files"));
482        }
483        let last = last_phase(opts.emit);
484        let linking = last == Phase::Link;
485
486        let mut kinds = Vec::with_capacity(inputs.len());
487        for input in inputs {
488            kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
489        }
490
491        // How many inputs actually write an output of their own. A `.o` on a `-c` line
492        // produces nothing, and neither does a `.s` on an `-E` line, so neither may count
493        // toward the `-o` check below. When linking there is exactly one output and it is the
494        // executable, so nothing counts.
495        let producing = if linking {
496            0
497        } else {
498            kinds
499                .iter()
500                .filter(|k| **k != InputKind::LinkerInput)
501                .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
502                .count()
503        };
504        if output.is_some() && !linking && producing > 1 {
505            return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
506        }
507
508        let mut notes = Vec::new();
509        let mut jobs = Vec::with_capacity(inputs.len());
510        let mut link_inputs = Vec::new();
511
512        for (input, kind) in inputs.iter().zip(kinds) {
513            // An object, an archive or a shared library has nothing done to it. It reaches the
514            // linker under the name it was written with, and its name is not derived from
515            // anything, which is why this case is separate rather than falling out of the
516            // sequence below. Deriving it would rewrite `libm.a` into `libm.o`.
517            if kind == InputKind::LinkerInput {
518                if linking {
519                    link_inputs.push(if input.library {
520                        Item::Library(input.path.clone())
521                    } else {
522                        Item::File(input.path.clone())
523                    });
524                } else {
525                    // GCC warns and carries on here, and configure scripts rely on that, so
526                    // this is a note rather than an error.
527                    notes.push(format!(
528                        "{}: linker input unused because linking was not requested",
529                        if input.library {
530                            format!("-l{}", input.path)
531                        } else {
532                            input.path.clone()
533                        }
534                    ));
535                }
536                // A library is not a file this compilation does anything to, so it gets no job.
537                // One would print a line under `-###` saying nothing happens to it, next to the
538                // note above already saying so.
539                if input.library {
540                    continue;
541                }
542                jobs.push(Job {
543                    input: input.path.clone(),
544                    kind,
545                    phases: Vec::new(),
546                    output: Output::File(input.path.clone()),
547                    aux_base: None,
548                });
549                continue;
550            }
551
552            let phases: Vec<Phase> =
553                kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
554            // `rucc -E a.s` lands here: assembly enters at `Assemble`, which is past where
555            // `-E` stops, so there is no phase left to run. GCC carries on rather than
556            // failing, and so do we.
557            let Some(&final_phase) = phases.last() else {
558                notes.push(format!(
559                    "{}: input unused because it enters the pipeline after the last phase \
560                     the mode flags asked for",
561                    input.path
562                ));
563                jobs.push(Job {
564                    input: input.path.clone(),
565                    kind,
566                    phases,
567                    output: Output::File(input.path.clone()),
568                    aux_base: None,
569                });
570                continue;
571            };
572            let named = if producing == 1 { output } else { None };
573            // A job that stops at the preprocessed text has nothing to keep, since that text is
574            // what it writes. Everything past it does: the text and, once there is a back end
575            // step after it, the assembly.
576            let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
577                .then(|| aux_base(opts, &input.path, output, linking));
578            let out = if final_phase == Phase::Link {
579                // The job stops at the object, and the link step below takes it from here. Under
580                // `-save-temps` the object is one of the files being kept, so it is written where
581                // the person can see it rather than in a directory that goes away.
582                let ext = suffix_for(Phase::Assemble, opts);
583                match &aux {
584                    Some(base) => Output::File(format!("{base}.{ext}")),
585                    None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
586                }
587            } else if let Some(o) = named {
588                // `-o -` is standard output rather than a file of that name, which is what gcc
589                // does for everything it compiles, the object file included. Its link step is
590                // the exception and writes a file called `-`, because the name goes to the
591                // linker and the linker takes it literally.
592                if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
593            } else if final_phase == Phase::Preprocess {
594                // `-E` writes to standard output unless it was given a name, which is the one
595                // place where the default is not a file.
596                Output::Stdout
597            } else {
598                Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
599            };
600            // An input whose output has the name it has itself would be read and then written
601            // over, and what it held would be gone. GCC compares the two names the way they
602            // were written and so does this, which catches `rucc --emit=ir a.ir` and leaves
603            // the same file reached by two different paths to the file system.
604            if let Output::File(path) = &out {
605                if *path == input.path {
606                    return Err(plan_err(format!(
607                        "input file `{}` is the same as the output file",
608                        input.path
609                    )));
610                }
611            }
612
613            if linking {
614                if let Some(p) = out.as_link_input() {
615                    link_inputs.push(Item::File(p.to_owned()));
616                }
617            }
618            jobs.push(Job { input: input.path.clone(), kind, phases, output: out, aux_base: aux });
619        }
620
621        let link = linking.then(|| LinkJob {
622            inputs: link_inputs,
623            output: output.unwrap_or(default_exe(opts)).to_owned(),
624        });
625
626        Ok(Plan { jobs, link, notes, output: output.map(str::to_owned) })
627    }
628
629    /// Renders the plan the way `-###` prints it.
630    ///
631    /// One line per job, then the link line. This is meant to be read next to `gcc -###`
632    /// output when a build behaves differently under the two compilers, so it says what will
633    /// happen rather than how it is represented.
634    #[must_use]
635    pub fn render(&self) -> String {
636        let mut out = String::new();
637        for note in &self.notes {
638            let _ = writeln!(out, "note: {note}");
639        }
640        for job in &self.jobs {
641            // A linker input has no phases of its own. It shows up in the link line below, or
642            // in a note above when there is no link line, and repeating it here would suggest
643            // something happens to it.
644            if job.phases.is_empty() {
645                continue;
646            }
647            let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
648            let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
649            // The files `-save-temps` keeps, which are as much a part of what will happen as the
650            // output is and are the only reason the flag was passed.
651            let kept: Vec<String> =
652                [job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
653            if !kept.is_empty() {
654                let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
655            }
656        }
657        if let Some(link) = &self.link {
658            let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
659            let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
660        }
661        out
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use rucc_session::Options;
668
669    use super::*;
670
671    fn opts(triple: &str) -> Options {
672        Options::new(triple.parse().expect("test triple"))
673    }
674
675    fn linux() -> Options {
676        opts("x86_64-unknown-linux-gnu")
677    }
678
679    fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
680        let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
681        Plan::new(o, &inputs, output).expect("expected a plan")
682    }
683
684    #[test]
685    fn extensions_map_to_the_table_in_the_spec() {
686        assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
687        assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
688        assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
689        assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
690        assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
691        assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
692        assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
693        assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
694        assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
695        assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
696    }
697
698    #[test]
699    fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
700        // It is the compiler's own output coming back in, so the phases in front of the walk
701        // have already happened to it and the ones after it are the ones still to run.
702        assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
703        assert_eq!(InputKind::Ir.as_str(), "ir");
704        assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
705        assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
706    }
707
708    #[test]
709    fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
710        // `rucc --emit=ir a.ir` would read the file and then write the result over it, and
711        // what it held would be gone.
712        let mut o = linux();
713        o.emit = EmitKind::Ir;
714        let inputs = [Input::new("a.ir")];
715        let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
716        assert!(error.message.contains("is the same as the output file"), "{error}");
717        // Naming it something else is fine, and so is the same name reached through `-o`
718        // being refused for the same reason.
719        assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
720        assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
721    }
722
723    #[test]
724    fn capital_s_and_small_s_are_different_languages() {
725        // On a case-insensitive file system it is tempting to fold these together. They are
726        // not the same: one runs the preprocessor and one does not.
727        let hi = InputKind::from_path("a.S").unwrap();
728        let lo = InputKind::from_path("a.s").unwrap();
729        assert_ne!(hi, lo);
730        assert!(hi.full_sequence().contains(&Phase::Preprocess));
731        assert!(!lo.full_sequence().contains(&Phase::Preprocess));
732    }
733
734    #[test]
735    fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
736        let e = InputKind::from_path("a.cpp").unwrap_err();
737        assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
738        let e = InputKind::from_x_arg("c++").unwrap_err();
739        assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
740    }
741
742    #[test]
743    fn a_file_with_no_extension_goes_to_the_linker() {
744        assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
745        assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
746    }
747
748    #[test]
749    fn the_default_line_compiles_and_links_to_a_out() {
750        let p = plan(&linux(), &["a.c"], None);
751        assert_eq!(
752            p.jobs[0].phases,
753            vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
754        );
755        assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
756        let link = p.link.expect("expected a link step");
757        assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
758        assert_eq!(link.output, "a.out");
759    }
760
761    #[test]
762    fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
763        let mut o = linux();
764        o.emit = EmitKind::Object;
765        let p = plan(&o, &["src/a.c", "src/b.c"], None);
766        assert!(p.link.is_none());
767        assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
768        assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
769        // Next to the source is what people expect and it is not what GCC does. The object
770        // lands in the current directory.
771        assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
772    }
773
774    #[test]
775    fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
776        let mut o = linux();
777        o.emit = EmitKind::Preprocessed;
778        assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
779        assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
780    }
781
782    #[test]
783    fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
784        let mut o = linux();
785        o.emit = EmitKind::Preprocessed;
786        assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
787        o.emit = EmitKind::Object;
788        assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
789        // The linker is handed the name and makes a file of it, which is gcc's behaviour and
790        // is the one place the dash is not standard output.
791        let p = plan(&linux(), &["a.c"], Some("-"));
792        assert_eq!(p.link.expect("a link step").output, "-");
793    }
794
795    #[test]
796    fn dash_s_produces_assembly_named_after_the_source() {
797        let mut o = linux();
798        o.emit = EmitKind::Asm;
799        let p = plan(&o, &["dir/a.c"], None);
800        assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
801        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
802    }
803
804    #[test]
805    fn an_already_preprocessed_file_skips_the_preprocessor() {
806        let p = plan(&linux(), &["a.i"], None);
807        assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
808    }
809
810    #[test]
811    fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
812        let p = plan(&linux(), &["a.S"], None);
813        assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
814        assert!(!p.jobs[0].phases.contains(&Phase::Compile));
815    }
816
817    #[test]
818    fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
819        // Link order is semantic. A plan that reorders it is a plan that produces a different
820        // program, and the failure would be a missing symbol nobody could explain.
821        let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
822        let link = p.link.expect("expected a link step");
823        assert_eq!(
824            link.inputs,
825            vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
826        );
827    }
828
829    #[test]
830    fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
831        // Configure scripts do this. Erroring here fails builds that work under GCC.
832        let mut o = linux();
833        o.emit = EmitKind::Object;
834        let p = plan(&o, &["a.c", "b.o"], None);
835        assert!(p.jobs[1].phases.is_empty());
836        assert_eq!(p.notes.len(), 1);
837        assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
838    }
839
840    #[test]
841    fn dash_o_with_several_compilations_is_rejected() {
842        let mut o = linux();
843        o.emit = EmitKind::Object;
844        let inputs = [Input::new("a.c"), Input::new("b.c")];
845        let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
846        assert!(e.message.contains("multiple inputs"), "{}", e.message);
847    }
848
849    #[test]
850    fn dash_o_with_one_compilation_and_some_objects_is_fine() {
851        // `rucc -c -o out.o a.c b.o` has exactly one thing to write, so the check above must
852        // not count the object.
853        let mut o = linux();
854        o.emit = EmitKind::Object;
855        let inputs = [Input::new("a.c"), Input::new("b.o")];
856        let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
857        assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
858    }
859
860    #[test]
861    fn dash_x_overrides_the_extension() {
862        let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), library: false }];
863        let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
864        assert_eq!(p.jobs[0].kind, InputKind::C);
865        assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
866    }
867
868    #[test]
869    fn windows_gets_obj_and_a_exe() {
870        let o = opts("x86_64-pc-windows-msvc");
871        let p = plan(&o, &["a.c"], None);
872        assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
873        assert_eq!(p.link.expect("expected a link step").output, "a.exe");
874    }
875
876    #[test]
877    fn the_intermediate_dumps_stop_where_dash_s_stops() {
878        for emit in [
879            EmitKind::Tast,
880            EmitKind::Ir,
881            EmitKind::MirFinal,
882            EmitKind::SafetySummary,
883            EmitKind::TypeGranules,
884        ] {
885            assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
886        }
887    }
888
889    #[test]
890    fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
891        // They all come out of the compile phase and none of them is assembly, so writing any
892        // of them to `a.s` would leave a file that neither an assembler nor a reader can use.
893        for (emit, name) in [
894            (EmitKind::Asm, "a.s"),
895            (EmitKind::Tast, "a.tast"),
896            (EmitKind::Ir, "a.ir"),
897            (EmitKind::MirFinal, "a.mir"),
898            (EmitKind::SafetySummary, "a.safety.json"),
899            (EmitKind::TypeGranules, "a.granules.txt"),
900        ] {
901            let mut o = linux();
902            o.emit = emit;
903            assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
904        }
905    }
906
907    #[test]
908    fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
909        // `rucc -E a.s` has nothing to preprocess. GCC carries on, and a configure script
910        // that probes with a mixed input list depends on that.
911        let mut o = linux();
912        o.emit = EmitKind::Preprocessed;
913        let p = plan(&o, &["a.c", "b.s"], None);
914        assert!(p.jobs[1].phases.is_empty());
915        assert_eq!(p.notes.len(), 1);
916        assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
917        // And it must not count against `-o`, because only one file is being written.
918        let inputs = [Input::new("a.c"), Input::new("b.s")];
919        assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
920    }
921
922    #[test]
923    fn no_inputs_is_an_error() {
924        assert!(Plan::new(&linux(), &[], None).is_err());
925    }
926
927    /// The plan for `paths` under `-save-temps` in the spelling `kind`.
928    fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
929        let mut o = linux();
930        o.emit = emit;
931        o.save_temps = kind;
932        plan(&o, paths, output)
933    }
934
935    /// What one job of that plan keeps, in the order the files are produced.
936    fn kept(plan: &Plan, at: usize) -> Vec<String> {
937        [plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
938    }
939
940    #[test]
941    fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
942        // gcc 16's bare `-save-temps` is `-save-temps=obj`, whatever its manual says, so
943        // `-o out/t.o` puts them in `out` and not in the working directory. Both spellings are
944        // here because the whole of the difference between them is the directory.
945        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
946        assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
947        let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
948        assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
949    }
950
951    #[test]
952    fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
953        // `-o out/x.o` keeps `x.i` and not `t.i`, and an output with no extension on it keeps
954        // the whole of the name it was given.
955        let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
956        assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
957        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
958        assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
959    }
960
961    #[test]
962    fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
963        // The object of `sub/u.c` is `u.o` in the working directory, so what is kept beside it
964        // is in the working directory as well, under both spellings.
965        for kind in [SaveTemps::Object, SaveTemps::Cwd] {
966            let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
967            assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
968            assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
969        }
970    }
971
972    #[test]
973    fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
974        // One output for however many inputs, so the input's own name goes on the end and two
975        // files that would otherwise both be `prog.i` are two files.
976        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
977        assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
978        assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
979        // With no `-o` the executable is `a.out`, and the `a` of it is what the files are named
980        // from, which is where `a-t.i` comes from on a command line nobody wrote an `a` on.
981        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
982        assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
983    }
984
985    #[test]
986    fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
987        // Without the flag it goes in a directory that is gone by the end of the run, and that
988        // is the one thing `-save-temps` cannot leave true: the object is one of the files it
989        // was asked to keep.
990        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
991        assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
992        let plain = plan(&linux(), &["t.c"], Some("out/prog"));
993        assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
994    }
995
996    #[test]
997    fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
998        // `-E` writes the preprocessed text, so there is nothing left over to keep, and `-S`
999        // writes the assembly and keeps only the text that came before it.
1000        let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
1001        assert_eq!(p.jobs[0].aux_base, None);
1002        assert_eq!(kept(&p, 0), Vec::<String>::new());
1003        let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
1004        assert_eq!(kept(&p, 0), vec!["t.i"]);
1005    }
1006
1007    #[test]
1008    fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
1009        // There is no phase 4 to keep the result of, and the file the compilation read is the
1010        // one that would have been written, which is already on the disk under its own name.
1011        let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1012        assert_eq!(kept(&p, 0), vec!["t.s"]);
1013        // And an input the linker takes directly goes through no step at all.
1014        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
1015        assert_eq!(p.jobs[0].aux_base, None);
1016    }
1017
1018    #[test]
1019    fn nothing_is_kept_when_the_flag_was_not_given() {
1020        let p = plan(&linux(), &["t.c"], None);
1021        assert_eq!(p.jobs[0].aux_base, None);
1022        assert_eq!(kept(&p, 0), Vec::<String>::new());
1023    }
1024
1025    #[test]
1026    fn the_rendering_says_what_will_happen() {
1027        let p = plan(&linux(), &["a.c", "b.o"], None);
1028        let text = p.render();
1029        assert!(
1030            text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
1031            "{text}"
1032        );
1033        assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
1034        // The object has nothing done to it, so it appears once, in the link line.
1035        assert_eq!(text.matches("b.o").count(), 1, "{text}");
1036    }
1037
1038    #[test]
1039    fn the_rendering_names_the_files_that_will_be_kept() {
1040        // `-###` is what will happen, and under `-save-temps` two more files being written is
1041        // part of that. It is also the only way to see the names without running a compilation.
1042        let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c"], None);
1043        let text = p.render();
1044        assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
1045        assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
1046    }
1047}