1use std::fmt::Write as _;
12
13use rucc_session::{EmitKind, Options, SaveTemps};
14use rucc_target::{Env, 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 pub stack_usage: Option<String>,
332}
333
334impl Job {
335 #[must_use]
341 pub fn saved_text(&self) -> Option<String> {
342 let base = self.aux_base.as_ref()?;
343 self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
344 }
345
346 #[must_use]
351 pub fn saved_asm(&self) -> Option<String> {
352 let base = self.aux_base.as_ref()?;
353 let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
354 (past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct LinkJob {
361 pub inputs: Vec<Item>,
363 pub output: String,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct ArchiveJob {
370 pub members: Vec<String>,
377 pub output: String,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Plan {
384 pub jobs: Vec<Job>,
386 pub link: Option<LinkJob>,
388 pub archive: Option<ArchiveJob>,
391 pub notes: Vec<String>,
394 pub output: Option<String>,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
404pub struct PlanError {
405 pub message: String,
407}
408
409impl std::fmt::Display for PlanError {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.write_str(&self.message)
412 }
413}
414
415impl std::error::Error for PlanError {}
416
417fn plan_err(message: impl Into<String>) -> PlanError {
418 PlanError { message: message.into() }
419}
420
421#[must_use]
426pub fn last_phase(emit: EmitKind) -> Phase {
427 match emit {
428 EmitKind::Preprocessed => Phase::Preprocess,
429 EmitKind::Asm
430 | EmitKind::Tast
431 | EmitKind::Ir
432 | EmitKind::MirFinal
433 | EmitKind::SafetySummary
434 | EmitKind::TypeGranules
435 | EmitKind::SyntaxOnly => Phase::Compile,
436 EmitKind::Object => Phase::Assemble,
437 EmitKind::Archive => Phase::Archive,
438 EmitKind::Executable => Phase::Link,
439 }
440}
441
442pub const STDIN: &str = "-";
444
445#[must_use]
450pub fn source_name(path: &str) -> &str {
451 if path == STDIN { "<stdin>" } else { path }
452}
453
454fn extension(path: &str) -> &str {
456 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
457 match name.rfind('.') {
458 Some(0) | None => "",
460 Some(i) => &name[i + 1..],
461 }
462}
463
464fn file_part(path: &str) -> &str {
466 path.rsplit(['/', '\\']).next().unwrap_or(path)
467}
468
469fn without_extension(path: &str) -> &str {
474 let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
475 match path[start..].rfind('.') {
476 Some(0) | None => path,
478 Some(i) => &path[..start + i],
479 }
480}
481
482fn stem(path: &str) -> &str {
485 file_part(without_extension(path))
486}
487
488fn aux_base(
512 opts: &Options,
513 input: &str,
514 output: Option<&str>,
515 collecting: bool,
516 alone: bool,
517) -> String {
518 let own = stem(input);
519 let named = if let Some(base) = opts.dump_base.as_deref() {
520 let ext = opts.dump_base_ext.as_deref().filter(|ext| !ext.is_empty());
521 let stripped = ext.and_then(|ext| base.strip_suffix(ext)).filter(|b| !b.is_empty());
522 let base = stripped.unwrap_or(base);
523 let base = if collecting { format!("{base}-{own}") } else { base.to_owned() };
524 if base.contains(['/', '\\']) {
525 base
526 } else {
527 let dir = opts.dump_dir.as_deref().unwrap_or_else(|| output.map_or("", directory));
528 format!("{dir}{base}")
529 }
530 } else if let Some(dir) = &opts.dump_dir {
531 let name = match output {
532 Some(o) if !collecting => file_part(without_extension(o)),
533 _ => own,
534 };
535 format!("{dir}{name}")
536 } else if collecting {
537 match output {
538 Some(o) => format!("{}-{own}", o.strip_suffix(".exe").unwrap_or(o)),
539 None if alone => own.to_owned(),
540 None => format!("{}-{own}", stem(default_exe(opts))),
541 }
542 } else {
543 output.map_or(own, without_extension).to_owned()
544 };
545 if opts.save_temps == SaveTemps::Cwd { file_part(&named).to_owned() } else { named }
546}
547
548fn directory(path: &str) -> &str {
550 path.rfind(['/', '\\']).map_or("", |i| &path[..=i])
551}
552
553fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
555 match phase {
556 Phase::Preprocess => "i",
557 Phase::Compile => match opts.emit {
561 EmitKind::Tast => "tast",
562 EmitKind::Ir => "ir",
563 EmitKind::MirFinal => "mir",
564 EmitKind::SafetySummary => "safety.json",
568 EmitKind::TypeGranules => "granules.txt",
571 _ => "s",
572 },
573 Phase::Assemble => {
576 if opts.target.os == Os::Windows {
577 "obj"
578 } else {
579 "o"
580 }
581 }
582 Phase::Archive | Phase::Link => "",
585 }
586}
587
588fn default_exe(opts: &Options) -> &'static str {
590 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
591}
592
593fn exe_name(opts: &Options, output: &str) -> String {
601 let mingw = opts.target.os == Os::Windows && opts.target.env == Env::Gnu;
602 let last = &output[directory(output).len()..];
603 if !mingw || last.contains('.') || output == STDIN || output == "/dev/null" {
604 return output.to_owned();
605 }
606 format!("{output}.exe")
607}
608
609impl Plan {
610 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
619 if inputs.is_empty() {
620 return Err(plan_err("no input files"));
621 }
622 let last = last_phase(opts.emit);
623 let linking = last == Phase::Link;
624 let archiving = last == Phase::Archive;
625 if archiving && output.is_none() {
629 return Err(plan_err("an archive has no default name, so `--emit=archive` needs `-o`"));
630 }
631 let collecting = linking || archiving;
634
635 let mut kinds = Vec::with_capacity(inputs.len());
636 for input in inputs {
637 if input.role == Role::File && input.path == STDIN && input.forced.is_none() {
642 if opts.emit != EmitKind::Preprocessed {
643 return Err(plan_err("-E or -x required when input is from standard input"));
644 }
645 kinds.push(InputKind::C);
646 continue;
647 }
648 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
649 }
650
651 let producing = if collecting {
656 0
657 } else {
658 kinds
659 .iter()
660 .filter(|k| **k != InputKind::LinkerInput)
661 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
662 .count()
663 };
664 if output.is_some() && !collecting && producing > 1 {
665 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
666 }
667
668 let alone = inputs.iter().filter(|input| input.role == Role::File).count() == 1;
672
673 let mut notes = Vec::new();
674 let mut jobs = Vec::with_capacity(inputs.len());
675 let mut link_inputs = Vec::new();
676 let mut members = Vec::new();
677
678 for (input, kind) in inputs.iter().zip(kinds) {
679 if input.role == Role::Linker {
685 if linking {
686 link_inputs.push(Item::Linker(input.path.clone()));
687 }
688 continue;
689 }
690
691 if kind == InputKind::LinkerInput {
696 if archiving {
703 let named = input.named();
704 return Err(plan_err(format!(
705 "{named}: an archive is written from the objects this command line \
706 compiles, and the symbol index in it needs the names each member \
707 defines, which this compiler knows for a file it compiled and not for \
708 one it was handed"
709 )));
710 }
711 if linking {
712 link_inputs.push(if input.role == Role::Library {
713 Item::Library(input.path.clone())
714 } else {
715 Item::File(input.path.clone())
716 });
717 } else {
718 notes.push(format!(
721 "{}: linker input unused because linking was not requested",
722 input.named()
723 ));
724 }
725 if input.role == Role::Library {
729 continue;
730 }
731 jobs.push(Job {
732 input: input.path.clone(),
733 kind,
734 phases: Vec::new(),
735 output: Output::File(input.path.clone()),
736 aux_base: None,
737 stack_usage: None,
738 });
739 continue;
740 }
741
742 let phases: Vec<Phase> =
743 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
744 let Some(&final_phase) = phases.last() else {
748 notes.push(format!(
749 "{}: input unused because it enters the pipeline after the last phase \
750 the mode flags asked for",
751 input.path
752 ));
753 jobs.push(Job {
754 input: input.path.clone(),
755 kind,
756 phases,
757 output: Output::File(input.path.clone()),
758 aux_base: None,
759 stack_usage: None,
760 });
761 continue;
762 };
763 let named = if producing == 1 { output } else { None };
764 let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
768 .then(|| aux_base(opts, &input.path, output, collecting, alone));
769 let out = if final_phase == Phase::Link || archiving {
770 let ext = suffix_for(Phase::Assemble, opts);
774 match &aux {
775 Some(base) => Output::File(format!("{base}.{ext}")),
776 None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
777 }
778 } else if opts.emit == EmitKind::SyntaxOnly {
779 Output::Stdout
782 } else if let Some(o) = named {
783 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
788 } else if final_phase == Phase::Preprocess {
789 Output::Stdout
792 } else {
793 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
794 };
795 if let Output::File(path) = &out {
803 if *path == input.path && path != "/dev/null" {
804 return Err(plan_err(format!(
805 "input file `{}` is the same as the output file",
806 input.path
807 )));
808 }
809 }
810
811 if linking {
812 if let Some(p) = out.as_link_input() {
813 link_inputs.push(Item::File(p.to_owned()));
814 }
815 }
816 if archiving {
817 members.push(format!(
821 "{}.{}",
822 stem(&input.path),
823 suffix_for(Phase::Assemble, opts)
824 ));
825 }
826 let syntax = opts.emit == EmitKind::SyntaxOnly;
833 let backed = syntax
834 || matches!(
835 opts.emit,
836 EmitKind::Asm | EmitKind::Object | EmitKind::Archive | EmitKind::Executable
837 );
838 let stack_usage = (opts.stack_usage
839 && backed
840 && kind != InputKind::Ir
841 && phases.contains(&Phase::Compile))
842 .then(|| {
843 let named = output.filter(|o| *o != "-");
844 format!("{}.su", aux_base(opts, &input.path, named, collecting || syntax, alone))
845 });
846 jobs.push(Job {
847 input: input.path.clone(),
848 kind,
849 phases,
850 output: out,
851 aux_base: aux,
852 stack_usage,
853 });
854 }
855
856 let link = linking.then(|| LinkJob {
857 inputs: link_inputs,
858 output: output.map_or_else(|| default_exe(opts).to_owned(), |o| exe_name(opts, o)),
859 });
860 let archive = archiving.then(|| ArchiveJob {
861 members,
862 output: output.unwrap_or_default().to_owned(),
865 });
866
867 Ok(Plan { jobs, link, archive, notes, output: output.map(str::to_owned) })
868 }
869
870 #[must_use]
876 pub fn render(&self) -> String {
877 let mut out = String::new();
878 for note in &self.notes {
879 let _ = writeln!(out, "note: {note}");
880 }
881 for job in &self.jobs {
882 if job.phases.is_empty() {
886 continue;
887 }
888 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
889 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
890 let kept: Vec<String> =
893 [job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
894 if !kept.is_empty() {
895 let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
896 }
897 }
898 if let Some(link) = &self.link {
899 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
900 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
901 }
902 if let Some(archive) = &self.archive {
903 let _ = writeln!(out, "archive: {} -> {}", archive.members.join(" "), archive.output);
904 }
905 out
906 }
907}
908
909#[cfg(test)]
910mod tests {
911 use rucc_session::Options;
912
913 use super::*;
914
915 fn opts(triple: &str) -> Options {
916 Options::new(triple.parse().expect("test triple"))
917 }
918
919 fn linux() -> Options {
920 opts("x86_64-unknown-linux-gnu")
921 }
922
923 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
924 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
925 Plan::new(o, &inputs, output).expect("expected a plan")
926 }
927
928 #[test]
929 fn extensions_map_to_the_table_in_the_spec() {
930 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
931 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
932 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
933 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
934 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
935 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
936 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
937 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
938 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
939 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
940 }
941
942 #[test]
943 fn standard_input_is_c_under_dash_e_and_wants_telling_otherwise() {
944 let mut o = linux();
949 o.emit = EmitKind::Preprocessed;
950 let plan = plan(&o, &["-"], None);
951 assert_eq!(plan.jobs.len(), 1);
952 assert_eq!(plan.jobs[0].kind, InputKind::C);
953 assert_eq!(plan.jobs[0].phases, [Phase::Preprocess]);
954 assert_eq!(plan.jobs[0].output, Output::Stdout);
955
956 let o = linux();
957 let error = Plan::new(&o, &[Input::new("-")], None).expect_err("expected this refused");
958 assert_eq!(error.message, "-E or -x required when input is from standard input");
959
960 let mut o = linux();
963 o.emit = EmitKind::Object;
964 let inputs = [Input { path: "-".to_owned(), forced: Some(InputKind::C), role: Role::File }];
965 let plan = Plan::new(&o, &inputs, None).expect("expected a plan");
966 assert_eq!(plan.jobs[0].kind, InputKind::C);
967 assert_eq!(plan.jobs[0].output, Output::File("-.o".to_owned()));
968 }
969
970 #[test]
971 fn standard_input_is_named_the_way_gcc_names_it_and_a_file_is_named_after_itself() {
972 assert_eq!(source_name("-"), "<stdin>");
973 assert_eq!(source_name("a.c"), "a.c");
974 assert_eq!(source_name("sub/-"), "sub/-");
975 }
976
977 #[test]
978 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
979 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
982 assert_eq!(InputKind::Ir.as_str(), "ir");
983 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
984 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
985 }
986
987 #[test]
988 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
989 let mut o = linux();
992 o.emit = EmitKind::Ir;
993 let inputs = [Input::new("a.ir")];
994 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
995 assert!(error.message.contains("is the same as the output file"), "{error}");
996 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
999 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
1000 let null = Input { forced: Some(InputKind::C), ..Input::new("/dev/null") };
1003 assert!(Plan::new(&linux(), &[null], Some("/dev/null")).is_ok());
1004 }
1005
1006 #[test]
1007 fn capital_s_and_small_s_are_different_languages() {
1008 let hi = InputKind::from_path("a.S").unwrap();
1011 let lo = InputKind::from_path("a.s").unwrap();
1012 assert_ne!(hi, lo);
1013 assert!(hi.full_sequence().contains(&Phase::Preprocess));
1014 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
1015 }
1016
1017 #[test]
1018 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
1019 let e = InputKind::from_path("a.cpp").unwrap_err();
1020 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
1021 let e = InputKind::from_x_arg("c++").unwrap_err();
1022 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
1023 }
1024
1025 #[test]
1026 fn a_file_with_no_extension_goes_to_the_linker() {
1027 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
1028 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
1029 }
1030
1031 #[test]
1032 fn the_default_line_compiles_and_links_to_a_out() {
1033 let p = plan(&linux(), &["a.c"], None);
1034 assert_eq!(
1035 p.jobs[0].phases,
1036 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
1037 );
1038 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
1039 let link = p.link.expect("expected a link step");
1040 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
1041 assert_eq!(link.output, "a.out");
1042 }
1043
1044 #[test]
1045 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
1046 let mut o = linux();
1047 o.emit = EmitKind::Object;
1048 let p = plan(&o, &["src/a.c", "src/b.c"], None);
1049 assert!(p.link.is_none());
1050 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
1051 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
1052 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
1055 }
1056
1057 #[test]
1058 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
1059 let mut o = linux();
1060 o.emit = EmitKind::Preprocessed;
1061 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
1062 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
1063 }
1064
1065 #[test]
1066 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
1067 let mut o = linux();
1068 o.emit = EmitKind::Preprocessed;
1069 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
1070 o.emit = EmitKind::Object;
1071 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
1072 let p = plan(&linux(), &["a.c"], Some("-"));
1075 assert_eq!(p.link.expect("a link step").output, "-");
1076 }
1077
1078 #[test]
1079 fn dash_s_produces_assembly_named_after_the_source() {
1080 let mut o = linux();
1081 o.emit = EmitKind::Asm;
1082 let p = plan(&o, &["dir/a.c"], None);
1083 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
1084 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
1085 }
1086
1087 #[test]
1088 fn an_already_preprocessed_file_skips_the_preprocessor() {
1089 let p = plan(&linux(), &["a.i"], None);
1090 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
1091 }
1092
1093 #[test]
1094 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
1095 let p = plan(&linux(), &["a.S"], None);
1096 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
1097 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
1098 }
1099
1100 #[test]
1101 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
1102 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
1105 let link = p.link.expect("expected a link step");
1106 assert_eq!(
1107 link.inputs,
1108 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
1109 );
1110 }
1111
1112 #[test]
1113 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
1114 let mut o = linux();
1116 o.emit = EmitKind::Object;
1117 let p = plan(&o, &["a.c", "b.o"], None);
1118 assert!(p.jobs[1].phases.is_empty());
1119 assert_eq!(p.notes.len(), 1);
1120 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
1121 }
1122
1123 #[test]
1124 fn dash_o_with_several_compilations_is_rejected() {
1125 let mut o = linux();
1126 o.emit = EmitKind::Object;
1127 let inputs = [Input::new("a.c"), Input::new("b.c")];
1128 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
1129 assert!(e.message.contains("multiple inputs"), "{}", e.message);
1130 }
1131
1132 #[test]
1133 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
1134 let mut o = linux();
1137 o.emit = EmitKind::Object;
1138 let inputs = [Input::new("a.c"), Input::new("b.o")];
1139 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
1140 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
1141 }
1142
1143 #[test]
1144 fn dash_x_overrides_the_extension() {
1145 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), role: Role::File }];
1146 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
1147 assert_eq!(p.jobs[0].kind, InputKind::C);
1148 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
1149 }
1150
1151 #[test]
1152 fn windows_gets_obj_and_a_exe() {
1153 let o = opts("x86_64-pc-windows-msvc");
1154 let p = plan(&o, &["a.c"], None);
1155 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
1156 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
1157 }
1158
1159 #[test]
1160 fn a_mingw_link_puts_exe_on_a_name_with_no_extension() {
1161 let exe = |triple: &str, output: &str| {
1164 plan(&opts(triple), &["a.c"], Some(output)).link.expect("expected a link step").output
1165 };
1166 let mingw = "x86_64-windows-gnu";
1167 assert_eq!(exe(mingw, "foo"), "foo.exe");
1168 assert_eq!(exe(mingw, "sub.d/foo"), "sub.d/foo.exe", "only the last component counts");
1169 assert_eq!(exe(mingw, r"sub.d\foo"), r"sub.d\foo.exe");
1170 assert_eq!(exe(mingw, "foo.exe"), "foo.exe");
1171 assert_eq!(exe(mingw, "a.b"), "a.b");
1172 assert_eq!(exe(mingw, "foo."), "foo.", "any dot at all is an extension to gcc");
1173 assert_eq!(exe(mingw, "-"), "-");
1174 assert_eq!(exe(mingw, "/dev/null"), "/dev/null");
1175 assert_eq!(exe("x86_64-pc-windows-msvc", "foo"), "foo");
1177 assert_eq!(exe("x86_64-unknown-linux-gnu", "foo"), "foo");
1178 for emit in [EmitKind::Object, EmitKind::Asm] {
1180 let mut o = opts(mingw);
1181 o.emit = emit;
1182 assert_eq!(plan(&o, &["a.c"], Some("foo")).jobs[0].output, Output::File("foo".into()));
1183 }
1184 }
1185
1186 #[test]
1188 fn an_archive_is_one_file_however_many_inputs_there_are() {
1189 let mut o = linux();
1190 o.emit = EmitKind::Archive;
1191 let p = plan(&o, &["a.c", "sub/b.c"], Some("out/libx.a"));
1192 assert_eq!(p.jobs.len(), 2);
1193 for job in &p.jobs {
1194 assert_eq!(job.phases.last(), Some(&Phase::Assemble), "{}", job.input);
1197 assert!(matches!(job.output, Output::Temporary(_)), "{:?}", job.output);
1198 }
1199 let archive = p.archive.expect("an archive step");
1200 assert_eq!(archive.members, ["a.o", "b.o"]);
1201 assert_eq!(archive.output, "out/libx.a");
1202 assert!(p.link.is_none(), "one command line produces one of the two and not both");
1203 }
1204
1205 #[test]
1206 fn a_member_is_called_what_an_object_is_called_on_this_target() {
1207 let mut o = opts("x86_64-pc-windows-msvc");
1208 o.emit = EmitKind::Archive;
1209 let p = plan(&o, &["a.c"], Some("x.lib"));
1210 assert_eq!(p.archive.expect("an archive step").members, ["a.obj"]);
1211 }
1212
1213 #[test]
1216 fn an_archive_has_no_default_name() {
1217 let mut o = linux();
1218 o.emit = EmitKind::Archive;
1219 let inputs = [Input::new("a.c")];
1220 let error = Plan::new(&o, &inputs, None).expect_err("no name for the archive");
1221 assert!(error.message.contains("needs `-o`"), "{error}");
1222 }
1223
1224 #[test]
1227 fn something_this_compilation_did_not_produce_cannot_go_into_an_archive() {
1228 let mut o = linux();
1229 o.emit = EmitKind::Archive;
1230 for handed in [Input::new("b.o"), Input::library("m")] {
1231 let inputs = [Input::new("a.c"), handed];
1232 let error = Plan::new(&o, &inputs, Some("libx.a")).expect_err("not ours to index");
1233 assert!(error.message.contains("names each member"), "{error}");
1234 }
1235 }
1236
1237 #[test]
1238 fn the_plan_says_what_goes_into_the_archive() {
1239 let mut o = linux();
1240 o.emit = EmitKind::Archive;
1241 let text = plan(&o, &["a.c", "b.c"], Some("libx.a")).render();
1242 assert!(text.contains("archive: a.o b.o -> libx.a"), "{text}");
1243 assert!(!text.contains("link:"), "{text}");
1244 }
1245
1246 #[test]
1247 fn the_intermediate_dumps_stop_where_dash_s_stops() {
1248 for emit in [
1249 EmitKind::Tast,
1250 EmitKind::Ir,
1251 EmitKind::MirFinal,
1252 EmitKind::SafetySummary,
1253 EmitKind::TypeGranules,
1254 ] {
1255 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
1256 }
1257 }
1258
1259 #[test]
1260 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
1261 for (emit, name) in [
1264 (EmitKind::Asm, "a.s"),
1265 (EmitKind::Tast, "a.tast"),
1266 (EmitKind::Ir, "a.ir"),
1267 (EmitKind::MirFinal, "a.mir"),
1268 (EmitKind::SafetySummary, "a.safety.json"),
1269 (EmitKind::TypeGranules, "a.granules.txt"),
1270 ] {
1271 let mut o = linux();
1272 o.emit = emit;
1273 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
1274 }
1275 }
1276
1277 #[test]
1278 fn syntax_only_writes_no_file_even_when_given_a_name() {
1279 let mut o = linux();
1280 o.emit = EmitKind::SyntaxOnly;
1281 assert_eq!(last_phase(o.emit), Phase::Compile);
1282 let p = plan(&o, &["a.c", "b.c"], None);
1283 assert!(p.jobs.iter().all(|job| job.output == Output::Stdout), "{:?}", p.jobs);
1284 assert_eq!(plan(&o, &["a.c"], Some("a.o")).jobs[0].output, Output::Stdout);
1285 assert!(!plan(&o, &["a.c"], None).render().contains("link:"));
1286 }
1287
1288 #[test]
1289 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
1290 let mut o = linux();
1293 o.emit = EmitKind::Preprocessed;
1294 let p = plan(&o, &["a.c", "b.s"], None);
1295 assert!(p.jobs[1].phases.is_empty());
1296 assert_eq!(p.notes.len(), 1);
1297 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
1298 let inputs = [Input::new("a.c"), Input::new("b.s")];
1300 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
1301 }
1302
1303 #[test]
1304 fn no_inputs_is_an_error() {
1305 assert!(Plan::new(&linux(), &[], None).is_err());
1306 }
1307
1308 fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
1310 let mut o = linux();
1311 o.emit = emit;
1312 o.save_temps = kind;
1313 plan(&o, paths, output)
1314 }
1315
1316 fn kept(plan: &Plan, at: usize) -> Vec<String> {
1318 [plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
1319 }
1320
1321 #[test]
1322 fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
1323 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
1327 assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
1328 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
1329 assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1330 }
1331
1332 #[test]
1333 fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
1334 let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
1337 assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
1338 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
1339 assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
1340 }
1341
1342 #[test]
1343 fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
1344 for kind in [SaveTemps::Object, SaveTemps::Cwd] {
1347 let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
1348 assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
1349 assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
1350 }
1351 }
1352
1353 #[test]
1354 fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
1355 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
1358 assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1359 assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
1360 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "u.c"], None);
1364 assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
1365 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
1366 assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
1367 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("o/p.so"));
1369 assert_eq!(kept(&p, 0), vec!["o/p.so-t.i", "o/p.so-t.s"]);
1370 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("o/p.exe"));
1371 assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
1372 }
1373
1374 #[test]
1375 fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
1376 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
1380 assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
1381 let plain = plan(&linux(), &["t.c"], Some("out/prog"));
1382 assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
1383 }
1384
1385 #[test]
1386 fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
1387 let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
1390 assert_eq!(p.jobs[0].aux_base, None);
1391 assert_eq!(kept(&p, 0), Vec::<String>::new());
1392 let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
1393 assert_eq!(kept(&p, 0), vec!["t.i"]);
1394 }
1395
1396 #[test]
1397 fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
1398 let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
1401 assert_eq!(kept(&p, 0), vec!["t.s"]);
1402 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
1404 assert_eq!(p.jobs[0].aux_base, None);
1405 }
1406
1407 #[test]
1408 fn nothing_is_kept_when_the_flag_was_not_given() {
1409 let p = plan(&linux(), &["t.c"], None);
1410 assert_eq!(p.jobs[0].aux_base, None);
1411 assert_eq!(kept(&p, 0), Vec::<String>::new());
1412 }
1413
1414 #[test]
1415 fn the_rendering_says_what_will_happen() {
1416 let p = plan(&linux(), &["a.c", "b.o"], None);
1417 let text = p.render();
1418 assert!(
1419 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
1420 "{text}"
1421 );
1422 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
1423 assert_eq!(text.matches("b.o").count(), 1, "{text}");
1425 }
1426
1427 #[test]
1428 fn the_rendering_names_the_files_that_will_be_kept() {
1429 let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c", "b.c"], None);
1432 let text = p.render();
1433 assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
1434 assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
1435 }
1436}