1use std::fmt::Write as _;
12
13use rucc_session::{EmitKind, Options, SaveTemps};
14use rucc_target::Os;
15
16use crate::link::Item;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum Phase {
26 Preprocess,
28 Compile,
30 Assemble,
32 Link,
34}
35
36impl Phase {
37 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum InputKind {
58 C,
60 CHeader,
62 PreprocessedC,
64 Ir,
72 Assembler,
74 AssemblerWithCpp,
77 LinkerInput,
79}
80
81impl InputKind {
82 #[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 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 pub fn from_path(path: &str) -> Result<InputKind, XError> {
128 let ext = extension(path);
129 match ext {
130 "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 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 InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
155 InputKind::Assembler => &[Assemble, Link],
156 InputKind::LinkerInput => &[Link],
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum XError {
164 Unknown(String),
166 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#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Input {
196 pub path: String,
198 pub forced: Option<InputKind>,
200 pub library: bool,
207}
208
209impl Input {
210 #[must_use]
212 pub fn new(path: impl Into<String>) -> Input {
213 Input { path: path.into(), forced: None, library: false }
214 }
215
216 #[must_use]
218 pub fn library(name: impl Into<String>) -> Input {
219 Input { path: name.into(), forced: None, library: true }
220 }
221
222 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#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum Output {
241 Stdout,
243 File(String),
245 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 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#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Job {
271 pub input: String,
273 pub kind: InputKind,
275 pub phases: Vec<Phase>,
277 pub output: Output,
279 pub aux_base: Option<String>,
286}
287
288impl Job {
289 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct LinkJob {
315 pub inputs: Vec<Item>,
317 pub output: String,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct Plan {
324 pub jobs: Vec<Job>,
326 pub link: Option<LinkJob>,
328 pub notes: Vec<String>,
331 pub output: Option<String>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct PlanError {
342 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#[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
377fn extension(path: &str) -> &str {
379 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
380 match name.rfind('.') {
381 Some(0) | None => "",
383 Some(i) => &name[i + 1..],
384 }
385}
386
387fn file_part(path: &str) -> &str {
389 path.rsplit(['/', '\\']).next().unwrap_or(path)
390}
391
392fn without_extension(path: &str) -> &str {
397 let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
398 match path[start..].rfind('.') {
399 Some(0) | None => path,
401 Some(i) => &path[..start + i],
402 }
403}
404
405fn stem(path: &str) -> &str {
408 file_part(without_extension(path))
409}
410
411fn aux_base(opts: &Options, input: &str, output: Option<&str>, linking: bool) -> String {
420 let named = match output {
421 Some(o) => without_extension(o),
422 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
432fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
434 match phase {
435 Phase::Preprocess => "i",
436 Phase::Compile => match opts.emit {
440 EmitKind::Tast => "tast",
441 EmitKind::Ir => "ir",
442 EmitKind::MirFinal => "mir",
443 EmitKind::SafetySummary => "safety.json",
447 EmitKind::TypeGranules => "granules.txt",
450 _ => "s",
451 },
452 Phase::Assemble => {
455 if opts.target.os == Os::Windows {
456 "obj"
457 } else {
458 "o"
459 }
460 }
461 Phase::Link => "",
462 }
463}
464
465fn default_exe(opts: &Options) -> &'static str {
467 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
468}
469
470impl Plan {
471 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 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 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 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 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 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 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 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 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
593 } else if final_phase == Phase::Preprocess {
594 Output::Stdout
597 } else {
598 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
599 };
600 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1012 assert_eq!(kept(&p, 0), vec!["t.s"]);
1013 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 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 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}