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 Archive,
39 Link,
41}
42
43impl Phase {
44 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum InputKind {
66 C,
68 CHeader,
70 PreprocessedC,
72 Ir,
80 Assembler,
82 AssemblerWithCpp,
85 LinkerInput,
87}
88
89impl InputKind {
90 #[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 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 pub fn from_path(path: &str) -> Result<InputKind, XError> {
136 let ext = extension(path);
137 match ext {
138 "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 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 InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
163 InputKind::Assembler => &[Assemble, Link],
164 InputKind::LinkerInput => &[Link],
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum XError {
172 Unknown(String),
174 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#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct Input {
204 pub path: String,
206 pub forced: Option<InputKind>,
208 pub library: bool,
215}
216
217impl Input {
218 #[must_use]
220 pub fn new(path: impl Into<String>) -> Input {
221 Input { path: path.into(), forced: None, library: false }
222 }
223
224 #[must_use]
226 pub fn library(name: impl Into<String>) -> Input {
227 Input { path: name.into(), forced: None, library: true }
228 }
229
230 pub fn kind(&self) -> Result<InputKind, XError> {
236 if self.library {
237 return Ok(InputKind::LinkerInput);
238 }
239 match self.forced {
240 Some(k) => Ok(k),
241 None => InputKind::from_path(&self.path),
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum Output {
249 Stdout,
251 File(String),
253 Temporary(String),
256}
257
258impl Output {
259 fn render(&self) -> String {
260 match self {
261 Output::Stdout => "-".to_owned(),
262 Output::File(p) => p.clone(),
263 Output::Temporary(p) => format!("{p} (temporary)"),
264 }
265 }
266
267 fn as_link_input(&self) -> Option<&str> {
269 match self {
270 Output::File(p) | Output::Temporary(p) => Some(p),
271 Output::Stdout => None,
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct Job {
279 pub input: String,
281 pub kind: InputKind,
283 pub phases: Vec<Phase>,
285 pub output: Output,
287 pub aux_base: Option<String>,
294}
295
296impl Job {
297 #[must_use]
303 pub fn saved_text(&self) -> Option<String> {
304 let base = self.aux_base.as_ref()?;
305 self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
306 }
307
308 #[must_use]
313 pub fn saved_asm(&self) -> Option<String> {
314 let base = self.aux_base.as_ref()?;
315 let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
316 (past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct LinkJob {
323 pub inputs: Vec<Item>,
325 pub output: String,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct ArchiveJob {
332 pub members: Vec<String>,
339 pub output: String,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct Plan {
346 pub jobs: Vec<Job>,
348 pub link: Option<LinkJob>,
350 pub archive: Option<ArchiveJob>,
353 pub notes: Vec<String>,
356 pub output: Option<String>,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct PlanError {
367 pub message: String,
369}
370
371impl std::fmt::Display for PlanError {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 f.write_str(&self.message)
374 }
375}
376
377impl std::error::Error for PlanError {}
378
379fn plan_err(message: impl Into<String>) -> PlanError {
380 PlanError { message: message.into() }
381}
382
383#[must_use]
388pub fn last_phase(emit: EmitKind) -> Phase {
389 match emit {
390 EmitKind::Preprocessed => Phase::Preprocess,
391 EmitKind::Asm
392 | EmitKind::Tast
393 | EmitKind::Ir
394 | EmitKind::MirFinal
395 | EmitKind::SafetySummary
396 | EmitKind::TypeGranules => Phase::Compile,
397 EmitKind::Object => Phase::Assemble,
398 EmitKind::Archive => Phase::Archive,
399 EmitKind::Executable => Phase::Link,
400 }
401}
402
403fn extension(path: &str) -> &str {
405 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
406 match name.rfind('.') {
407 Some(0) | None => "",
409 Some(i) => &name[i + 1..],
410 }
411}
412
413fn file_part(path: &str) -> &str {
415 path.rsplit(['/', '\\']).next().unwrap_or(path)
416}
417
418fn without_extension(path: &str) -> &str {
423 let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
424 match path[start..].rfind('.') {
425 Some(0) | None => path,
427 Some(i) => &path[..start + i],
428 }
429}
430
431fn stem(path: &str) -> &str {
434 file_part(without_extension(path))
435}
436
437fn aux_base(opts: &Options, input: &str, output: Option<&str>, collecting: bool) -> String {
449 let named = match output {
450 Some(o) => without_extension(o),
451 None if collecting => stem(default_exe(opts)),
455 None => stem(input),
456 };
457 let named = if opts.save_temps == SaveTemps::Cwd { file_part(named) } else { named };
458 if collecting { format!("{named}-{}", stem(input)) } else { named.to_owned() }
459}
460
461fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
463 match phase {
464 Phase::Preprocess => "i",
465 Phase::Compile => match opts.emit {
469 EmitKind::Tast => "tast",
470 EmitKind::Ir => "ir",
471 EmitKind::MirFinal => "mir",
472 EmitKind::SafetySummary => "safety.json",
476 EmitKind::TypeGranules => "granules.txt",
479 _ => "s",
480 },
481 Phase::Assemble => {
484 if opts.target.os == Os::Windows {
485 "obj"
486 } else {
487 "o"
488 }
489 }
490 Phase::Archive | Phase::Link => "",
493 }
494}
495
496fn default_exe(opts: &Options) -> &'static str {
498 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
499}
500
501impl Plan {
502 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
511 if inputs.is_empty() {
512 return Err(plan_err("no input files"));
513 }
514 let last = last_phase(opts.emit);
515 let linking = last == Phase::Link;
516 let archiving = last == Phase::Archive;
517 if archiving && output.is_none() {
521 return Err(plan_err("an archive has no default name, so `--emit=archive` needs `-o`"));
522 }
523 let collecting = linking || archiving;
526
527 let mut kinds = Vec::with_capacity(inputs.len());
528 for input in inputs {
529 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
530 }
531
532 let producing = if collecting {
537 0
538 } else {
539 kinds
540 .iter()
541 .filter(|k| **k != InputKind::LinkerInput)
542 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
543 .count()
544 };
545 if output.is_some() && !collecting && producing > 1 {
546 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
547 }
548
549 let mut notes = Vec::new();
550 let mut jobs = Vec::with_capacity(inputs.len());
551 let mut link_inputs = Vec::new();
552 let mut members = Vec::new();
553
554 for (input, kind) in inputs.iter().zip(kinds) {
555 if kind == InputKind::LinkerInput {
560 if archiving {
567 let named = if input.library {
568 format!("-l{}", input.path)
569 } else {
570 input.path.clone()
571 };
572 return Err(plan_err(format!(
573 "{named}: an archive is written from the objects this command line \
574 compiles, and the symbol index in it needs the names each member \
575 defines, which this compiler knows for a file it compiled and not for \
576 one it was handed"
577 )));
578 }
579 if linking {
580 link_inputs.push(if input.library {
581 Item::Library(input.path.clone())
582 } else {
583 Item::File(input.path.clone())
584 });
585 } else {
586 notes.push(format!(
589 "{}: linker input unused because linking was not requested",
590 if input.library {
591 format!("-l{}", input.path)
592 } else {
593 input.path.clone()
594 }
595 ));
596 }
597 if input.library {
601 continue;
602 }
603 jobs.push(Job {
604 input: input.path.clone(),
605 kind,
606 phases: Vec::new(),
607 output: Output::File(input.path.clone()),
608 aux_base: None,
609 });
610 continue;
611 }
612
613 let phases: Vec<Phase> =
614 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
615 let Some(&final_phase) = phases.last() else {
619 notes.push(format!(
620 "{}: input unused because it enters the pipeline after the last phase \
621 the mode flags asked for",
622 input.path
623 ));
624 jobs.push(Job {
625 input: input.path.clone(),
626 kind,
627 phases,
628 output: Output::File(input.path.clone()),
629 aux_base: None,
630 });
631 continue;
632 };
633 let named = if producing == 1 { output } else { None };
634 let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
638 .then(|| aux_base(opts, &input.path, output, collecting));
639 let out = if final_phase == Phase::Link || archiving {
640 let ext = suffix_for(Phase::Assemble, opts);
644 match &aux {
645 Some(base) => Output::File(format!("{base}.{ext}")),
646 None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
647 }
648 } else if let Some(o) = named {
649 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
654 } else if final_phase == Phase::Preprocess {
655 Output::Stdout
658 } else {
659 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
660 };
661 if let Output::File(path) = &out {
666 if *path == input.path {
667 return Err(plan_err(format!(
668 "input file `{}` is the same as the output file",
669 input.path
670 )));
671 }
672 }
673
674 if linking {
675 if let Some(p) = out.as_link_input() {
676 link_inputs.push(Item::File(p.to_owned()));
677 }
678 }
679 if archiving {
680 members.push(format!(
684 "{}.{}",
685 stem(&input.path),
686 suffix_for(Phase::Assemble, opts)
687 ));
688 }
689 jobs.push(Job { input: input.path.clone(), kind, phases, output: out, aux_base: aux });
690 }
691
692 let link = linking.then(|| LinkJob {
693 inputs: link_inputs,
694 output: output.unwrap_or(default_exe(opts)).to_owned(),
695 });
696 let archive = archiving.then(|| ArchiveJob {
697 members,
698 output: output.unwrap_or_default().to_owned(),
701 });
702
703 Ok(Plan { jobs, link, archive, notes, output: output.map(str::to_owned) })
704 }
705
706 #[must_use]
712 pub fn render(&self) -> String {
713 let mut out = String::new();
714 for note in &self.notes {
715 let _ = writeln!(out, "note: {note}");
716 }
717 for job in &self.jobs {
718 if job.phases.is_empty() {
722 continue;
723 }
724 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
725 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
726 let kept: Vec<String> =
729 [job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
730 if !kept.is_empty() {
731 let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
732 }
733 }
734 if let Some(link) = &self.link {
735 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
736 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
737 }
738 if let Some(archive) = &self.archive {
739 let _ = writeln!(out, "archive: {} -> {}", archive.members.join(" "), archive.output);
740 }
741 out
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use rucc_session::Options;
748
749 use super::*;
750
751 fn opts(triple: &str) -> Options {
752 Options::new(triple.parse().expect("test triple"))
753 }
754
755 fn linux() -> Options {
756 opts("x86_64-unknown-linux-gnu")
757 }
758
759 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
760 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
761 Plan::new(o, &inputs, output).expect("expected a plan")
762 }
763
764 #[test]
765 fn extensions_map_to_the_table_in_the_spec() {
766 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
767 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
768 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
769 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
770 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
771 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
772 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
773 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
774 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
775 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
776 }
777
778 #[test]
779 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
780 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
783 assert_eq!(InputKind::Ir.as_str(), "ir");
784 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
785 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
786 }
787
788 #[test]
789 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
790 let mut o = linux();
793 o.emit = EmitKind::Ir;
794 let inputs = [Input::new("a.ir")];
795 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
796 assert!(error.message.contains("is the same as the output file"), "{error}");
797 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
800 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
801 }
802
803 #[test]
804 fn capital_s_and_small_s_are_different_languages() {
805 let hi = InputKind::from_path("a.S").unwrap();
808 let lo = InputKind::from_path("a.s").unwrap();
809 assert_ne!(hi, lo);
810 assert!(hi.full_sequence().contains(&Phase::Preprocess));
811 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
812 }
813
814 #[test]
815 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
816 let e = InputKind::from_path("a.cpp").unwrap_err();
817 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
818 let e = InputKind::from_x_arg("c++").unwrap_err();
819 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
820 }
821
822 #[test]
823 fn a_file_with_no_extension_goes_to_the_linker() {
824 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
825 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
826 }
827
828 #[test]
829 fn the_default_line_compiles_and_links_to_a_out() {
830 let p = plan(&linux(), &["a.c"], None);
831 assert_eq!(
832 p.jobs[0].phases,
833 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
834 );
835 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
836 let link = p.link.expect("expected a link step");
837 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
838 assert_eq!(link.output, "a.out");
839 }
840
841 #[test]
842 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
843 let mut o = linux();
844 o.emit = EmitKind::Object;
845 let p = plan(&o, &["src/a.c", "src/b.c"], None);
846 assert!(p.link.is_none());
847 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
848 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
849 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
852 }
853
854 #[test]
855 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
856 let mut o = linux();
857 o.emit = EmitKind::Preprocessed;
858 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
859 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
860 }
861
862 #[test]
863 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
864 let mut o = linux();
865 o.emit = EmitKind::Preprocessed;
866 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
867 o.emit = EmitKind::Object;
868 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
869 let p = plan(&linux(), &["a.c"], Some("-"));
872 assert_eq!(p.link.expect("a link step").output, "-");
873 }
874
875 #[test]
876 fn dash_s_produces_assembly_named_after_the_source() {
877 let mut o = linux();
878 o.emit = EmitKind::Asm;
879 let p = plan(&o, &["dir/a.c"], None);
880 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
881 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
882 }
883
884 #[test]
885 fn an_already_preprocessed_file_skips_the_preprocessor() {
886 let p = plan(&linux(), &["a.i"], None);
887 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
888 }
889
890 #[test]
891 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
892 let p = plan(&linux(), &["a.S"], None);
893 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
894 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
895 }
896
897 #[test]
898 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
899 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
902 let link = p.link.expect("expected a link step");
903 assert_eq!(
904 link.inputs,
905 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
906 );
907 }
908
909 #[test]
910 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
911 let mut o = linux();
913 o.emit = EmitKind::Object;
914 let p = plan(&o, &["a.c", "b.o"], None);
915 assert!(p.jobs[1].phases.is_empty());
916 assert_eq!(p.notes.len(), 1);
917 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
918 }
919
920 #[test]
921 fn dash_o_with_several_compilations_is_rejected() {
922 let mut o = linux();
923 o.emit = EmitKind::Object;
924 let inputs = [Input::new("a.c"), Input::new("b.c")];
925 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
926 assert!(e.message.contains("multiple inputs"), "{}", e.message);
927 }
928
929 #[test]
930 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
931 let mut o = linux();
934 o.emit = EmitKind::Object;
935 let inputs = [Input::new("a.c"), Input::new("b.o")];
936 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
937 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
938 }
939
940 #[test]
941 fn dash_x_overrides_the_extension() {
942 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), library: false }];
943 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
944 assert_eq!(p.jobs[0].kind, InputKind::C);
945 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
946 }
947
948 #[test]
949 fn windows_gets_obj_and_a_exe() {
950 let o = opts("x86_64-pc-windows-msvc");
951 let p = plan(&o, &["a.c"], None);
952 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
953 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
954 }
955
956 #[test]
958 fn an_archive_is_one_file_however_many_inputs_there_are() {
959 let mut o = linux();
960 o.emit = EmitKind::Archive;
961 let p = plan(&o, &["a.c", "sub/b.c"], Some("out/libx.a"));
962 assert_eq!(p.jobs.len(), 2);
963 for job in &p.jobs {
964 assert_eq!(job.phases.last(), Some(&Phase::Assemble), "{}", job.input);
967 assert!(matches!(job.output, Output::Temporary(_)), "{:?}", job.output);
968 }
969 let archive = p.archive.expect("an archive step");
970 assert_eq!(archive.members, ["a.o", "b.o"]);
971 assert_eq!(archive.output, "out/libx.a");
972 assert!(p.link.is_none(), "one command line produces one of the two and not both");
973 }
974
975 #[test]
976 fn a_member_is_called_what_an_object_is_called_on_this_target() {
977 let mut o = opts("x86_64-pc-windows-msvc");
978 o.emit = EmitKind::Archive;
979 let p = plan(&o, &["a.c"], Some("x.lib"));
980 assert_eq!(p.archive.expect("an archive step").members, ["a.obj"]);
981 }
982
983 #[test]
986 fn an_archive_has_no_default_name() {
987 let mut o = linux();
988 o.emit = EmitKind::Archive;
989 let inputs = [Input::new("a.c")];
990 let error = Plan::new(&o, &inputs, None).expect_err("no name for the archive");
991 assert!(error.message.contains("needs `-o`"), "{error}");
992 }
993
994 #[test]
997 fn something_this_compilation_did_not_produce_cannot_go_into_an_archive() {
998 let mut o = linux();
999 o.emit = EmitKind::Archive;
1000 for handed in [Input::new("b.o"), Input::library("m")] {
1001 let inputs = [Input::new("a.c"), handed];
1002 let error = Plan::new(&o, &inputs, Some("libx.a")).expect_err("not ours to index");
1003 assert!(error.message.contains("names each member"), "{error}");
1004 }
1005 }
1006
1007 #[test]
1008 fn the_plan_says_what_goes_into_the_archive() {
1009 let mut o = linux();
1010 o.emit = EmitKind::Archive;
1011 let text = plan(&o, &["a.c", "b.c"], Some("libx.a")).render();
1012 assert!(text.contains("archive: a.o b.o -> libx.a"), "{text}");
1013 assert!(!text.contains("link:"), "{text}");
1014 }
1015
1016 #[test]
1017 fn the_intermediate_dumps_stop_where_dash_s_stops() {
1018 for emit in [
1019 EmitKind::Tast,
1020 EmitKind::Ir,
1021 EmitKind::MirFinal,
1022 EmitKind::SafetySummary,
1023 EmitKind::TypeGranules,
1024 ] {
1025 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
1026 }
1027 }
1028
1029 #[test]
1030 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
1031 for (emit, name) in [
1034 (EmitKind::Asm, "a.s"),
1035 (EmitKind::Tast, "a.tast"),
1036 (EmitKind::Ir, "a.ir"),
1037 (EmitKind::MirFinal, "a.mir"),
1038 (EmitKind::SafetySummary, "a.safety.json"),
1039 (EmitKind::TypeGranules, "a.granules.txt"),
1040 ] {
1041 let mut o = linux();
1042 o.emit = emit;
1043 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
1044 }
1045 }
1046
1047 #[test]
1048 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
1049 let mut o = linux();
1052 o.emit = EmitKind::Preprocessed;
1053 let p = plan(&o, &["a.c", "b.s"], None);
1054 assert!(p.jobs[1].phases.is_empty());
1055 assert_eq!(p.notes.len(), 1);
1056 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
1057 let inputs = [Input::new("a.c"), Input::new("b.s")];
1059 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
1060 }
1061
1062 #[test]
1063 fn no_inputs_is_an_error() {
1064 assert!(Plan::new(&linux(), &[], None).is_err());
1065 }
1066
1067 fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
1069 let mut o = linux();
1070 o.emit = emit;
1071 o.save_temps = kind;
1072 plan(&o, paths, output)
1073 }
1074
1075 fn kept(plan: &Plan, at: usize) -> Vec<String> {
1077 [plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
1078 }
1079
1080 #[test]
1081 fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
1082 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
1086 assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
1087 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
1088 assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1089 }
1090
1091 #[test]
1092 fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
1093 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
1096 assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
1097 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
1098 assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
1099 }
1100
1101 #[test]
1102 fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
1103 for kind in [SaveTemps::Object, SaveTemps::Cwd] {
1106 let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
1107 assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
1108 assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
1109 }
1110 }
1111
1112 #[test]
1113 fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
1114 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
1117 assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1118 assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
1119 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
1122 assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
1123 }
1124
1125 #[test]
1126 fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
1127 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
1131 assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
1132 let plain = plan(&linux(), &["t.c"], Some("out/prog"));
1133 assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
1134 }
1135
1136 #[test]
1137 fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
1138 let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
1141 assert_eq!(p.jobs[0].aux_base, None);
1142 assert_eq!(kept(&p, 0), Vec::<String>::new());
1143 let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
1144 assert_eq!(kept(&p, 0), vec!["t.i"]);
1145 }
1146
1147 #[test]
1148 fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
1149 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1152 assert_eq!(kept(&p, 0), vec!["t.s"]);
1153 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
1155 assert_eq!(p.jobs[0].aux_base, None);
1156 }
1157
1158 #[test]
1159 fn nothing_is_kept_when_the_flag_was_not_given() {
1160 let p = plan(&linux(), &["t.c"], None);
1161 assert_eq!(p.jobs[0].aux_base, None);
1162 assert_eq!(kept(&p, 0), Vec::<String>::new());
1163 }
1164
1165 #[test]
1166 fn the_rendering_says_what_will_happen() {
1167 let p = plan(&linux(), &["a.c", "b.o"], None);
1168 let text = p.render();
1169 assert!(
1170 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
1171 "{text}"
1172 );
1173 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
1174 assert_eq!(text.matches("b.o").count(), 1, "{text}");
1176 }
1177
1178 #[test]
1179 fn the_rendering_names_the_files_that_will_be_kept() {
1180 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c"], None);
1183 let text = p.render();
1184 assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
1185 assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
1186 }
1187}