1use std::fmt::Write as _;
12
13use rucc_session::{EmitKind, Options};
14use rucc_target::Os;
15
16use crate::link::Item;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum Phase {
26 Preprocess,
28 Compile,
30 Assemble,
32 Link,
34}
35
36impl Phase {
37 #[must_use]
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Phase::Preprocess => "preprocess",
42 Phase::Compile => "compile",
43 Phase::Assemble => "assemble",
44 Phase::Link => "link",
45 }
46 }
47}
48
49impl std::fmt::Display for Phase {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum InputKind {
58 C,
60 CHeader,
62 PreprocessedC,
64 Ir,
72 Assembler,
74 AssemblerWithCpp,
77 LinkerInput,
79}
80
81impl InputKind {
82 #[must_use]
84 pub fn as_str(self) -> &'static str {
85 match self {
86 InputKind::C => "c",
87 InputKind::CHeader => "c-header",
88 InputKind::PreprocessedC => "cpp-output",
89 InputKind::Ir => "ir",
90 InputKind::Assembler => "assembler",
91 InputKind::AssemblerWithCpp => "assembler-with-cpp",
92 InputKind::LinkerInput => "linker-input",
93 }
94 }
95
96 pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
103 match name {
104 "c" => Ok(InputKind::C),
105 "c-header" => Ok(InputKind::CHeader),
106 "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
107 "ir" => Ok(InputKind::Ir),
108 "assembler" => Ok(InputKind::Assembler),
109 "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
110 "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
111 Err(XError::Unsupported(name.to_owned()))
112 }
113 _ => Err(XError::Unknown(name.to_owned())),
114 }
115 }
116
117 pub fn from_path(path: &str) -> Result<InputKind, XError> {
128 let ext = extension(path);
129 match ext {
130 "c" => Ok(InputKind::C),
134 "i" => Ok(InputKind::PreprocessedC),
135 "ir" => Ok(InputKind::Ir),
136 "h" => Ok(InputKind::CHeader),
137 "s" => Ok(InputKind::Assembler),
138 "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
139 "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
140 Err(XError::Unsupported(ext.to_owned()))
141 }
142 _ => Ok(InputKind::LinkerInput),
143 }
144 }
145
146 fn full_sequence(self) -> &'static [Phase] {
148 use Phase::{Assemble, Compile, Link, Preprocess};
149 match self {
150 InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
151 InputKind::PreprocessedC | InputKind::Ir => &[Compile, Assemble, Link],
152 InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
155 InputKind::Assembler => &[Assemble, Link],
156 InputKind::LinkerInput => &[Link],
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum XError {
164 Unknown(String),
166 Unsupported(String),
168}
169
170impl std::fmt::Display for XError {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 match self {
173 XError::Unknown(name) => {
174 write!(
175 f,
176 "unknown language `{name}`; \
177 accepted: c, c-header, cpp-output, ir, assembler, assembler-with-cpp, none"
178 )
179 }
180 XError::Unsupported(name) => {
181 write!(
182 f,
183 "`{name}` is not C, and this compiler is only ever going to compile C; \
184 see the not-in-scope list in spec/00-README.md"
185 )
186 }
187 }
188 }
189}
190
191impl std::error::Error for XError {}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Input {
196 pub path: String,
198 pub forced: Option<InputKind>,
200 pub library: bool,
207}
208
209impl Input {
210 #[must_use]
212 pub fn new(path: impl Into<String>) -> Input {
213 Input { path: path.into(), forced: None, library: false }
214 }
215
216 #[must_use]
218 pub fn library(name: impl Into<String>) -> Input {
219 Input { path: name.into(), forced: None, library: true }
220 }
221
222 pub fn kind(&self) -> Result<InputKind, XError> {
228 if self.library {
229 return Ok(InputKind::LinkerInput);
230 }
231 match self.forced {
232 Some(k) => Ok(k),
233 None => InputKind::from_path(&self.path),
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub enum Output {
241 Stdout,
243 File(String),
245 Temporary(String),
248}
249
250impl Output {
251 fn render(&self) -> String {
252 match self {
253 Output::Stdout => "-".to_owned(),
254 Output::File(p) => p.clone(),
255 Output::Temporary(p) => format!("{p} (temporary)"),
256 }
257 }
258
259 fn as_link_input(&self) -> Option<&str> {
261 match self {
262 Output::File(p) | Output::Temporary(p) => Some(p),
263 Output::Stdout => None,
264 }
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Job {
271 pub input: String,
273 pub kind: InputKind,
275 pub phases: Vec<Phase>,
277 pub output: Output,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct LinkJob {
284 pub inputs: Vec<Item>,
286 pub output: String,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct Plan {
293 pub jobs: Vec<Job>,
295 pub link: Option<LinkJob>,
297 pub notes: Vec<String>,
300 pub output: Option<String>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct PlanError {
311 pub message: String,
313}
314
315impl std::fmt::Display for PlanError {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 f.write_str(&self.message)
318 }
319}
320
321impl std::error::Error for PlanError {}
322
323fn plan_err(message: impl Into<String>) -> PlanError {
324 PlanError { message: message.into() }
325}
326
327#[must_use]
332pub fn last_phase(emit: EmitKind) -> Phase {
333 match emit {
334 EmitKind::Preprocessed => Phase::Preprocess,
335 EmitKind::Asm
336 | EmitKind::Tast
337 | EmitKind::Ir
338 | EmitKind::MirFinal
339 | EmitKind::SafetySummary
340 | EmitKind::TypeGranules => Phase::Compile,
341 EmitKind::Object => Phase::Assemble,
342 EmitKind::Executable => Phase::Link,
343 }
344}
345
346fn extension(path: &str) -> &str {
348 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
349 match name.rfind('.') {
350 Some(0) | None => "",
352 Some(i) => &name[i + 1..],
353 }
354}
355
356fn stem(path: &str) -> &str {
359 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
360 match name.rfind('.') {
361 Some(0) | None => name,
362 Some(i) => &name[..i],
363 }
364}
365
366fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
368 match phase {
369 Phase::Preprocess => "i",
370 Phase::Compile => match opts.emit {
374 EmitKind::Tast => "tast",
375 EmitKind::Ir => "ir",
376 EmitKind::MirFinal => "mir",
377 EmitKind::SafetySummary => "safety.json",
381 EmitKind::TypeGranules => "granules.txt",
384 _ => "s",
385 },
386 Phase::Assemble => {
389 if opts.target.os == Os::Windows {
390 "obj"
391 } else {
392 "o"
393 }
394 }
395 Phase::Link => "",
396 }
397}
398
399fn default_exe(opts: &Options) -> &'static str {
401 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
402}
403
404impl Plan {
405 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
414 if inputs.is_empty() {
415 return Err(plan_err("no input files"));
416 }
417 let last = last_phase(opts.emit);
418 let linking = last == Phase::Link;
419
420 let mut kinds = Vec::with_capacity(inputs.len());
421 for input in inputs {
422 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
423 }
424
425 let producing = if linking {
430 0
431 } else {
432 kinds
433 .iter()
434 .filter(|k| **k != InputKind::LinkerInput)
435 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
436 .count()
437 };
438 if output.is_some() && !linking && producing > 1 {
439 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
440 }
441
442 let mut notes = Vec::new();
443 let mut jobs = Vec::with_capacity(inputs.len());
444 let mut link_inputs = Vec::new();
445
446 for (input, kind) in inputs.iter().zip(kinds) {
447 if kind == InputKind::LinkerInput {
452 if linking {
453 link_inputs.push(if input.library {
454 Item::Library(input.path.clone())
455 } else {
456 Item::File(input.path.clone())
457 });
458 } else {
459 notes.push(format!(
462 "{}: linker input unused because linking was not requested",
463 if input.library {
464 format!("-l{}", input.path)
465 } else {
466 input.path.clone()
467 }
468 ));
469 }
470 if input.library {
474 continue;
475 }
476 jobs.push(Job {
477 input: input.path.clone(),
478 kind,
479 phases: Vec::new(),
480 output: Output::File(input.path.clone()),
481 });
482 continue;
483 }
484
485 let phases: Vec<Phase> =
486 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
487 let Some(&final_phase) = phases.last() else {
491 notes.push(format!(
492 "{}: input unused because it enters the pipeline after the last phase \
493 the mode flags asked for",
494 input.path
495 ));
496 jobs.push(Job {
497 input: input.path.clone(),
498 kind,
499 phases,
500 output: Output::File(input.path.clone()),
501 });
502 continue;
503 };
504 let named = if producing == 1 { output } else { None };
505 let out = if final_phase == Phase::Link {
506 let ext = suffix_for(Phase::Assemble, opts);
508 Output::Temporary(format!("{}.{ext}", stem(&input.path)))
509 } else if let Some(o) = named {
510 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
515 } else if final_phase == Phase::Preprocess {
516 Output::Stdout
519 } else {
520 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
521 };
522 if let Output::File(path) = &out {
527 if *path == input.path {
528 return Err(plan_err(format!(
529 "input file `{}` is the same as the output file",
530 input.path
531 )));
532 }
533 }
534
535 if linking {
536 if let Some(p) = out.as_link_input() {
537 link_inputs.push(Item::File(p.to_owned()));
538 }
539 }
540 jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
541 }
542
543 let link = linking.then(|| LinkJob {
544 inputs: link_inputs,
545 output: output.unwrap_or(default_exe(opts)).to_owned(),
546 });
547
548 Ok(Plan { jobs, link, notes, output: output.map(str::to_owned) })
549 }
550
551 #[must_use]
557 pub fn render(&self) -> String {
558 let mut out = String::new();
559 for note in &self.notes {
560 let _ = writeln!(out, "note: {note}");
561 }
562 for job in &self.jobs {
563 if job.phases.is_empty() {
567 continue;
568 }
569 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
570 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
571 }
572 if let Some(link) = &self.link {
573 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
574 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
575 }
576 out
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use rucc_session::Options;
583
584 use super::*;
585
586 fn opts(triple: &str) -> Options {
587 Options::new(triple.parse().expect("test triple"))
588 }
589
590 fn linux() -> Options {
591 opts("x86_64-unknown-linux-gnu")
592 }
593
594 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
595 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
596 Plan::new(o, &inputs, output).expect("expected a plan")
597 }
598
599 #[test]
600 fn extensions_map_to_the_table_in_the_spec() {
601 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
602 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
603 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
604 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
605 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
606 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
607 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
608 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
609 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
610 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
611 }
612
613 #[test]
614 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
615 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
618 assert_eq!(InputKind::Ir.as_str(), "ir");
619 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
620 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
621 }
622
623 #[test]
624 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
625 let mut o = linux();
628 o.emit = EmitKind::Ir;
629 let inputs = [Input::new("a.ir")];
630 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
631 assert!(error.message.contains("is the same as the output file"), "{error}");
632 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
635 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
636 }
637
638 #[test]
639 fn capital_s_and_small_s_are_different_languages() {
640 let hi = InputKind::from_path("a.S").unwrap();
643 let lo = InputKind::from_path("a.s").unwrap();
644 assert_ne!(hi, lo);
645 assert!(hi.full_sequence().contains(&Phase::Preprocess));
646 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
647 }
648
649 #[test]
650 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
651 let e = InputKind::from_path("a.cpp").unwrap_err();
652 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
653 let e = InputKind::from_x_arg("c++").unwrap_err();
654 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
655 }
656
657 #[test]
658 fn a_file_with_no_extension_goes_to_the_linker() {
659 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
660 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
661 }
662
663 #[test]
664 fn the_default_line_compiles_and_links_to_a_out() {
665 let p = plan(&linux(), &["a.c"], None);
666 assert_eq!(
667 p.jobs[0].phases,
668 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
669 );
670 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
671 let link = p.link.expect("expected a link step");
672 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
673 assert_eq!(link.output, "a.out");
674 }
675
676 #[test]
677 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
678 let mut o = linux();
679 o.emit = EmitKind::Object;
680 let p = plan(&o, &["src/a.c", "src/b.c"], None);
681 assert!(p.link.is_none());
682 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
683 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
684 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
687 }
688
689 #[test]
690 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
691 let mut o = linux();
692 o.emit = EmitKind::Preprocessed;
693 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
694 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
695 }
696
697 #[test]
698 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
699 let mut o = linux();
700 o.emit = EmitKind::Preprocessed;
701 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
702 o.emit = EmitKind::Object;
703 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
704 let p = plan(&linux(), &["a.c"], Some("-"));
707 assert_eq!(p.link.expect("a link step").output, "-");
708 }
709
710 #[test]
711 fn dash_s_produces_assembly_named_after_the_source() {
712 let mut o = linux();
713 o.emit = EmitKind::Asm;
714 let p = plan(&o, &["dir/a.c"], None);
715 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
716 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
717 }
718
719 #[test]
720 fn an_already_preprocessed_file_skips_the_preprocessor() {
721 let p = plan(&linux(), &["a.i"], None);
722 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
723 }
724
725 #[test]
726 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
727 let p = plan(&linux(), &["a.S"], None);
728 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
729 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
730 }
731
732 #[test]
733 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
734 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
737 let link = p.link.expect("expected a link step");
738 assert_eq!(
739 link.inputs,
740 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
741 );
742 }
743
744 #[test]
745 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
746 let mut o = linux();
748 o.emit = EmitKind::Object;
749 let p = plan(&o, &["a.c", "b.o"], None);
750 assert!(p.jobs[1].phases.is_empty());
751 assert_eq!(p.notes.len(), 1);
752 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
753 }
754
755 #[test]
756 fn dash_o_with_several_compilations_is_rejected() {
757 let mut o = linux();
758 o.emit = EmitKind::Object;
759 let inputs = [Input::new("a.c"), Input::new("b.c")];
760 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
761 assert!(e.message.contains("multiple inputs"), "{}", e.message);
762 }
763
764 #[test]
765 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
766 let mut o = linux();
769 o.emit = EmitKind::Object;
770 let inputs = [Input::new("a.c"), Input::new("b.o")];
771 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
772 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
773 }
774
775 #[test]
776 fn dash_x_overrides_the_extension() {
777 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), library: false }];
778 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
779 assert_eq!(p.jobs[0].kind, InputKind::C);
780 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
781 }
782
783 #[test]
784 fn windows_gets_obj_and_a_exe() {
785 let o = opts("x86_64-pc-windows-msvc");
786 let p = plan(&o, &["a.c"], None);
787 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
788 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
789 }
790
791 #[test]
792 fn the_intermediate_dumps_stop_where_dash_s_stops() {
793 for emit in [
794 EmitKind::Tast,
795 EmitKind::Ir,
796 EmitKind::MirFinal,
797 EmitKind::SafetySummary,
798 EmitKind::TypeGranules,
799 ] {
800 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
801 }
802 }
803
804 #[test]
805 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
806 for (emit, name) in [
809 (EmitKind::Asm, "a.s"),
810 (EmitKind::Tast, "a.tast"),
811 (EmitKind::Ir, "a.ir"),
812 (EmitKind::MirFinal, "a.mir"),
813 (EmitKind::SafetySummary, "a.safety.json"),
814 (EmitKind::TypeGranules, "a.granules.txt"),
815 ] {
816 let mut o = linux();
817 o.emit = emit;
818 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
819 }
820 }
821
822 #[test]
823 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
824 let mut o = linux();
827 o.emit = EmitKind::Preprocessed;
828 let p = plan(&o, &["a.c", "b.s"], None);
829 assert!(p.jobs[1].phases.is_empty());
830 assert_eq!(p.notes.len(), 1);
831 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
832 let inputs = [Input::new("a.c"), Input::new("b.s")];
834 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
835 }
836
837 #[test]
838 fn no_inputs_is_an_error() {
839 assert!(Plan::new(&linux(), &[], None).is_err());
840 }
841
842 #[test]
843 fn the_rendering_says_what_will_happen() {
844 let p = plan(&linux(), &["a.c", "b.o"], None);
845 let text = p.render();
846 assert!(
847 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
848 "{text}"
849 );
850 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
851 assert_eq!(text.matches("b.o").count(), 1, "{text}");
853 }
854}