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, Copy, PartialEq, Eq)]
210pub enum Role {
211 File,
213 Library,
215 Linker,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct Input {
222 pub path: String,
225 pub forced: Option<InputKind>,
227 pub role: Role,
229}
230
231impl Input {
232 #[must_use]
234 pub fn new(path: impl Into<String>) -> Input {
235 Input { path: path.into(), forced: None, role: Role::File }
236 }
237
238 #[must_use]
240 pub fn library(name: impl Into<String>) -> Input {
241 Input { path: name.into(), forced: None, role: Role::Library }
242 }
243
244 #[must_use]
246 pub fn linker(arg: impl Into<String>) -> Input {
247 Input { path: arg.into(), forced: None, role: Role::Linker }
248 }
249
250 #[must_use]
252 pub fn named(&self) -> String {
253 match self.role {
254 Role::File => self.path.clone(),
255 Role::Library => format!("-l{}", self.path),
256 Role::Linker => format!("-Wl,{}", self.path),
257 }
258 }
259
260 pub fn kind(&self) -> Result<InputKind, XError> {
266 if self.role != Role::File {
267 return Ok(InputKind::LinkerInput);
268 }
269 match self.forced {
270 Some(k) => Ok(k),
271 None => InputKind::from_path(&self.path),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub enum Output {
279 Stdout,
281 File(String),
283 Temporary(String),
286}
287
288impl Output {
289 fn render(&self) -> String {
290 match self {
291 Output::Stdout => "-".to_owned(),
292 Output::File(p) => p.clone(),
293 Output::Temporary(p) => format!("{p} (temporary)"),
294 }
295 }
296
297 fn as_link_input(&self) -> Option<&str> {
299 match self {
300 Output::File(p) | Output::Temporary(p) => Some(p),
301 Output::Stdout => None,
302 }
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct Job {
309 pub input: String,
311 pub kind: InputKind,
313 pub phases: Vec<Phase>,
315 pub output: Output,
317 pub aux_base: Option<String>,
324}
325
326impl Job {
327 #[must_use]
333 pub fn saved_text(&self) -> Option<String> {
334 let base = self.aux_base.as_ref()?;
335 self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
336 }
337
338 #[must_use]
343 pub fn saved_asm(&self) -> Option<String> {
344 let base = self.aux_base.as_ref()?;
345 let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
346 (past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct LinkJob {
353 pub inputs: Vec<Item>,
355 pub output: String,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct ArchiveJob {
362 pub members: Vec<String>,
369 pub output: String,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct Plan {
376 pub jobs: Vec<Job>,
378 pub link: Option<LinkJob>,
380 pub archive: Option<ArchiveJob>,
383 pub notes: Vec<String>,
386 pub output: Option<String>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct PlanError {
397 pub message: String,
399}
400
401impl std::fmt::Display for PlanError {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 f.write_str(&self.message)
404 }
405}
406
407impl std::error::Error for PlanError {}
408
409fn plan_err(message: impl Into<String>) -> PlanError {
410 PlanError { message: message.into() }
411}
412
413#[must_use]
418pub fn last_phase(emit: EmitKind) -> Phase {
419 match emit {
420 EmitKind::Preprocessed => Phase::Preprocess,
421 EmitKind::Asm
422 | EmitKind::Tast
423 | EmitKind::Ir
424 | EmitKind::MirFinal
425 | EmitKind::SafetySummary
426 | EmitKind::TypeGranules => Phase::Compile,
427 EmitKind::Object => Phase::Assemble,
428 EmitKind::Archive => Phase::Archive,
429 EmitKind::Executable => Phase::Link,
430 }
431}
432
433fn extension(path: &str) -> &str {
435 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
436 match name.rfind('.') {
437 Some(0) | None => "",
439 Some(i) => &name[i + 1..],
440 }
441}
442
443fn file_part(path: &str) -> &str {
445 path.rsplit(['/', '\\']).next().unwrap_or(path)
446}
447
448fn without_extension(path: &str) -> &str {
453 let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
454 match path[start..].rfind('.') {
455 Some(0) | None => path,
457 Some(i) => &path[..start + i],
458 }
459}
460
461fn stem(path: &str) -> &str {
464 file_part(without_extension(path))
465}
466
467fn aux_base(opts: &Options, input: &str, output: Option<&str>, collecting: bool) -> String {
479 let named = match output {
480 Some(o) => without_extension(o),
481 None if collecting => stem(default_exe(opts)),
485 None => stem(input),
486 };
487 let named = if opts.save_temps == SaveTemps::Cwd { file_part(named) } else { named };
488 if collecting { format!("{named}-{}", stem(input)) } else { named.to_owned() }
489}
490
491fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
493 match phase {
494 Phase::Preprocess => "i",
495 Phase::Compile => match opts.emit {
499 EmitKind::Tast => "tast",
500 EmitKind::Ir => "ir",
501 EmitKind::MirFinal => "mir",
502 EmitKind::SafetySummary => "safety.json",
506 EmitKind::TypeGranules => "granules.txt",
509 _ => "s",
510 },
511 Phase::Assemble => {
514 if opts.target.os == Os::Windows {
515 "obj"
516 } else {
517 "o"
518 }
519 }
520 Phase::Archive | Phase::Link => "",
523 }
524}
525
526fn default_exe(opts: &Options) -> &'static str {
528 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
529}
530
531impl Plan {
532 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
541 if inputs.is_empty() {
542 return Err(plan_err("no input files"));
543 }
544 let last = last_phase(opts.emit);
545 let linking = last == Phase::Link;
546 let archiving = last == Phase::Archive;
547 if archiving && output.is_none() {
551 return Err(plan_err("an archive has no default name, so `--emit=archive` needs `-o`"));
552 }
553 let collecting = linking || archiving;
556
557 let mut kinds = Vec::with_capacity(inputs.len());
558 for input in inputs {
559 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
560 }
561
562 let producing = if collecting {
567 0
568 } else {
569 kinds
570 .iter()
571 .filter(|k| **k != InputKind::LinkerInput)
572 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
573 .count()
574 };
575 if output.is_some() && !collecting && producing > 1 {
576 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
577 }
578
579 let mut notes = Vec::new();
580 let mut jobs = Vec::with_capacity(inputs.len());
581 let mut link_inputs = Vec::new();
582 let mut members = Vec::new();
583
584 for (input, kind) in inputs.iter().zip(kinds) {
585 if input.role == Role::Linker {
591 if linking {
592 link_inputs.push(Item::Linker(input.path.clone()));
593 }
594 continue;
595 }
596
597 if kind == InputKind::LinkerInput {
602 if archiving {
609 let named = input.named();
610 return Err(plan_err(format!(
611 "{named}: an archive is written from the objects this command line \
612 compiles, and the symbol index in it needs the names each member \
613 defines, which this compiler knows for a file it compiled and not for \
614 one it was handed"
615 )));
616 }
617 if linking {
618 link_inputs.push(if input.role == Role::Library {
619 Item::Library(input.path.clone())
620 } else {
621 Item::File(input.path.clone())
622 });
623 } else {
624 notes.push(format!(
627 "{}: linker input unused because linking was not requested",
628 input.named()
629 ));
630 }
631 if input.role == Role::Library {
635 continue;
636 }
637 jobs.push(Job {
638 input: input.path.clone(),
639 kind,
640 phases: Vec::new(),
641 output: Output::File(input.path.clone()),
642 aux_base: None,
643 });
644 continue;
645 }
646
647 let phases: Vec<Phase> =
648 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
649 let Some(&final_phase) = phases.last() else {
653 notes.push(format!(
654 "{}: input unused because it enters the pipeline after the last phase \
655 the mode flags asked for",
656 input.path
657 ));
658 jobs.push(Job {
659 input: input.path.clone(),
660 kind,
661 phases,
662 output: Output::File(input.path.clone()),
663 aux_base: None,
664 });
665 continue;
666 };
667 let named = if producing == 1 { output } else { None };
668 let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
672 .then(|| aux_base(opts, &input.path, output, collecting));
673 let out = if final_phase == Phase::Link || archiving {
674 let ext = suffix_for(Phase::Assemble, opts);
678 match &aux {
679 Some(base) => Output::File(format!("{base}.{ext}")),
680 None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
681 }
682 } else if let Some(o) = named {
683 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
688 } else if final_phase == Phase::Preprocess {
689 Output::Stdout
692 } else {
693 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
694 };
695 if let Output::File(path) = &out {
700 if *path == input.path {
701 return Err(plan_err(format!(
702 "input file `{}` is the same as the output file",
703 input.path
704 )));
705 }
706 }
707
708 if linking {
709 if let Some(p) = out.as_link_input() {
710 link_inputs.push(Item::File(p.to_owned()));
711 }
712 }
713 if archiving {
714 members.push(format!(
718 "{}.{}",
719 stem(&input.path),
720 suffix_for(Phase::Assemble, opts)
721 ));
722 }
723 jobs.push(Job { input: input.path.clone(), kind, phases, output: out, aux_base: aux });
724 }
725
726 let link = linking.then(|| LinkJob {
727 inputs: link_inputs,
728 output: output.unwrap_or(default_exe(opts)).to_owned(),
729 });
730 let archive = archiving.then(|| ArchiveJob {
731 members,
732 output: output.unwrap_or_default().to_owned(),
735 });
736
737 Ok(Plan { jobs, link, archive, notes, output: output.map(str::to_owned) })
738 }
739
740 #[must_use]
746 pub fn render(&self) -> String {
747 let mut out = String::new();
748 for note in &self.notes {
749 let _ = writeln!(out, "note: {note}");
750 }
751 for job in &self.jobs {
752 if job.phases.is_empty() {
756 continue;
757 }
758 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
759 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
760 let kept: Vec<String> =
763 [job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
764 if !kept.is_empty() {
765 let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
766 }
767 }
768 if let Some(link) = &self.link {
769 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
770 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
771 }
772 if let Some(archive) = &self.archive {
773 let _ = writeln!(out, "archive: {} -> {}", archive.members.join(" "), archive.output);
774 }
775 out
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use rucc_session::Options;
782
783 use super::*;
784
785 fn opts(triple: &str) -> Options {
786 Options::new(triple.parse().expect("test triple"))
787 }
788
789 fn linux() -> Options {
790 opts("x86_64-unknown-linux-gnu")
791 }
792
793 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
794 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
795 Plan::new(o, &inputs, output).expect("expected a plan")
796 }
797
798 #[test]
799 fn extensions_map_to_the_table_in_the_spec() {
800 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
801 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
802 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
803 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
804 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
805 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
806 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
807 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
808 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
809 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
810 }
811
812 #[test]
813 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
814 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
817 assert_eq!(InputKind::Ir.as_str(), "ir");
818 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
819 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
820 }
821
822 #[test]
823 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
824 let mut o = linux();
827 o.emit = EmitKind::Ir;
828 let inputs = [Input::new("a.ir")];
829 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
830 assert!(error.message.contains("is the same as the output file"), "{error}");
831 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
834 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
835 }
836
837 #[test]
838 fn capital_s_and_small_s_are_different_languages() {
839 let hi = InputKind::from_path("a.S").unwrap();
842 let lo = InputKind::from_path("a.s").unwrap();
843 assert_ne!(hi, lo);
844 assert!(hi.full_sequence().contains(&Phase::Preprocess));
845 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
846 }
847
848 #[test]
849 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
850 let e = InputKind::from_path("a.cpp").unwrap_err();
851 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
852 let e = InputKind::from_x_arg("c++").unwrap_err();
853 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
854 }
855
856 #[test]
857 fn a_file_with_no_extension_goes_to_the_linker() {
858 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
859 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
860 }
861
862 #[test]
863 fn the_default_line_compiles_and_links_to_a_out() {
864 let p = plan(&linux(), &["a.c"], None);
865 assert_eq!(
866 p.jobs[0].phases,
867 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
868 );
869 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
870 let link = p.link.expect("expected a link step");
871 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
872 assert_eq!(link.output, "a.out");
873 }
874
875 #[test]
876 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
877 let mut o = linux();
878 o.emit = EmitKind::Object;
879 let p = plan(&o, &["src/a.c", "src/b.c"], None);
880 assert!(p.link.is_none());
881 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
882 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
883 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
886 }
887
888 #[test]
889 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
890 let mut o = linux();
891 o.emit = EmitKind::Preprocessed;
892 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
893 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
894 }
895
896 #[test]
897 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
898 let mut o = linux();
899 o.emit = EmitKind::Preprocessed;
900 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
901 o.emit = EmitKind::Object;
902 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
903 let p = plan(&linux(), &["a.c"], Some("-"));
906 assert_eq!(p.link.expect("a link step").output, "-");
907 }
908
909 #[test]
910 fn dash_s_produces_assembly_named_after_the_source() {
911 let mut o = linux();
912 o.emit = EmitKind::Asm;
913 let p = plan(&o, &["dir/a.c"], None);
914 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
915 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
916 }
917
918 #[test]
919 fn an_already_preprocessed_file_skips_the_preprocessor() {
920 let p = plan(&linux(), &["a.i"], None);
921 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
922 }
923
924 #[test]
925 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
926 let p = plan(&linux(), &["a.S"], None);
927 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
928 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
929 }
930
931 #[test]
932 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
933 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
936 let link = p.link.expect("expected a link step");
937 assert_eq!(
938 link.inputs,
939 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
940 );
941 }
942
943 #[test]
944 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
945 let mut o = linux();
947 o.emit = EmitKind::Object;
948 let p = plan(&o, &["a.c", "b.o"], None);
949 assert!(p.jobs[1].phases.is_empty());
950 assert_eq!(p.notes.len(), 1);
951 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
952 }
953
954 #[test]
955 fn dash_o_with_several_compilations_is_rejected() {
956 let mut o = linux();
957 o.emit = EmitKind::Object;
958 let inputs = [Input::new("a.c"), Input::new("b.c")];
959 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
960 assert!(e.message.contains("multiple inputs"), "{}", e.message);
961 }
962
963 #[test]
964 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
965 let mut o = linux();
968 o.emit = EmitKind::Object;
969 let inputs = [Input::new("a.c"), Input::new("b.o")];
970 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
971 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
972 }
973
974 #[test]
975 fn dash_x_overrides_the_extension() {
976 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), role: Role::File }];
977 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
978 assert_eq!(p.jobs[0].kind, InputKind::C);
979 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
980 }
981
982 #[test]
983 fn windows_gets_obj_and_a_exe() {
984 let o = opts("x86_64-pc-windows-msvc");
985 let p = plan(&o, &["a.c"], None);
986 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
987 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
988 }
989
990 #[test]
992 fn an_archive_is_one_file_however_many_inputs_there_are() {
993 let mut o = linux();
994 o.emit = EmitKind::Archive;
995 let p = plan(&o, &["a.c", "sub/b.c"], Some("out/libx.a"));
996 assert_eq!(p.jobs.len(), 2);
997 for job in &p.jobs {
998 assert_eq!(job.phases.last(), Some(&Phase::Assemble), "{}", job.input);
1001 assert!(matches!(job.output, Output::Temporary(_)), "{:?}", job.output);
1002 }
1003 let archive = p.archive.expect("an archive step");
1004 assert_eq!(archive.members, ["a.o", "b.o"]);
1005 assert_eq!(archive.output, "out/libx.a");
1006 assert!(p.link.is_none(), "one command line produces one of the two and not both");
1007 }
1008
1009 #[test]
1010 fn a_member_is_called_what_an_object_is_called_on_this_target() {
1011 let mut o = opts("x86_64-pc-windows-msvc");
1012 o.emit = EmitKind::Archive;
1013 let p = plan(&o, &["a.c"], Some("x.lib"));
1014 assert_eq!(p.archive.expect("an archive step").members, ["a.obj"]);
1015 }
1016
1017 #[test]
1020 fn an_archive_has_no_default_name() {
1021 let mut o = linux();
1022 o.emit = EmitKind::Archive;
1023 let inputs = [Input::new("a.c")];
1024 let error = Plan::new(&o, &inputs, None).expect_err("no name for the archive");
1025 assert!(error.message.contains("needs `-o`"), "{error}");
1026 }
1027
1028 #[test]
1031 fn something_this_compilation_did_not_produce_cannot_go_into_an_archive() {
1032 let mut o = linux();
1033 o.emit = EmitKind::Archive;
1034 for handed in [Input::new("b.o"), Input::library("m")] {
1035 let inputs = [Input::new("a.c"), handed];
1036 let error = Plan::new(&o, &inputs, Some("libx.a")).expect_err("not ours to index");
1037 assert!(error.message.contains("names each member"), "{error}");
1038 }
1039 }
1040
1041 #[test]
1042 fn the_plan_says_what_goes_into_the_archive() {
1043 let mut o = linux();
1044 o.emit = EmitKind::Archive;
1045 let text = plan(&o, &["a.c", "b.c"], Some("libx.a")).render();
1046 assert!(text.contains("archive: a.o b.o -> libx.a"), "{text}");
1047 assert!(!text.contains("link:"), "{text}");
1048 }
1049
1050 #[test]
1051 fn the_intermediate_dumps_stop_where_dash_s_stops() {
1052 for emit in [
1053 EmitKind::Tast,
1054 EmitKind::Ir,
1055 EmitKind::MirFinal,
1056 EmitKind::SafetySummary,
1057 EmitKind::TypeGranules,
1058 ] {
1059 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
1060 }
1061 }
1062
1063 #[test]
1064 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
1065 for (emit, name) in [
1068 (EmitKind::Asm, "a.s"),
1069 (EmitKind::Tast, "a.tast"),
1070 (EmitKind::Ir, "a.ir"),
1071 (EmitKind::MirFinal, "a.mir"),
1072 (EmitKind::SafetySummary, "a.safety.json"),
1073 (EmitKind::TypeGranules, "a.granules.txt"),
1074 ] {
1075 let mut o = linux();
1076 o.emit = emit;
1077 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
1078 }
1079 }
1080
1081 #[test]
1082 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
1083 let mut o = linux();
1086 o.emit = EmitKind::Preprocessed;
1087 let p = plan(&o, &["a.c", "b.s"], None);
1088 assert!(p.jobs[1].phases.is_empty());
1089 assert_eq!(p.notes.len(), 1);
1090 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
1091 let inputs = [Input::new("a.c"), Input::new("b.s")];
1093 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
1094 }
1095
1096 #[test]
1097 fn no_inputs_is_an_error() {
1098 assert!(Plan::new(&linux(), &[], None).is_err());
1099 }
1100
1101 fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
1103 let mut o = linux();
1104 o.emit = emit;
1105 o.save_temps = kind;
1106 plan(&o, paths, output)
1107 }
1108
1109 fn kept(plan: &Plan, at: usize) -> Vec<String> {
1111 [plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
1112 }
1113
1114 #[test]
1115 fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
1116 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
1120 assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
1121 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
1122 assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1123 }
1124
1125 #[test]
1126 fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
1127 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
1130 assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
1131 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
1132 assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
1133 }
1134
1135 #[test]
1136 fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
1137 for kind in [SaveTemps::Object, SaveTemps::Cwd] {
1140 let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
1141 assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
1142 assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
1143 }
1144 }
1145
1146 #[test]
1147 fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
1148 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
1151 assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1152 assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
1153 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
1156 assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
1157 }
1158
1159 #[test]
1160 fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
1161 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
1165 assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
1166 let plain = plan(&linux(), &["t.c"], Some("out/prog"));
1167 assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
1168 }
1169
1170 #[test]
1171 fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
1172 let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
1175 assert_eq!(p.jobs[0].aux_base, None);
1176 assert_eq!(kept(&p, 0), Vec::<String>::new());
1177 let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
1178 assert_eq!(kept(&p, 0), vec!["t.i"]);
1179 }
1180
1181 #[test]
1182 fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
1183 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1186 assert_eq!(kept(&p, 0), vec!["t.s"]);
1187 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
1189 assert_eq!(p.jobs[0].aux_base, None);
1190 }
1191
1192 #[test]
1193 fn nothing_is_kept_when_the_flag_was_not_given() {
1194 let p = plan(&linux(), &["t.c"], None);
1195 assert_eq!(p.jobs[0].aux_base, None);
1196 assert_eq!(kept(&p, 0), Vec::<String>::new());
1197 }
1198
1199 #[test]
1200 fn the_rendering_says_what_will_happen() {
1201 let p = plan(&linux(), &["a.c", "b.o"], None);
1202 let text = p.render();
1203 assert!(
1204 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
1205 "{text}"
1206 );
1207 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
1208 assert_eq!(text.matches("b.o").count(), 1, "{text}");
1210 }
1211
1212 #[test]
1213 fn the_rendering_names_the_files_that_will_be_kept() {
1214 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c"], None);
1217 let text = p.render();
1218 assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
1219 assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
1220 }
1221}