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}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct PlanError {
305 pub message: String,
307}
308
309impl std::fmt::Display for PlanError {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 f.write_str(&self.message)
312 }
313}
314
315impl std::error::Error for PlanError {}
316
317fn plan_err(message: impl Into<String>) -> PlanError {
318 PlanError { message: message.into() }
319}
320
321#[must_use]
326pub fn last_phase(emit: EmitKind) -> Phase {
327 match emit {
328 EmitKind::Preprocessed => Phase::Preprocess,
329 EmitKind::Asm
330 | EmitKind::Tast
331 | EmitKind::Ir
332 | EmitKind::MirFinal
333 | EmitKind::SafetySummary
334 | EmitKind::TypeGranules => Phase::Compile,
335 EmitKind::Object => Phase::Assemble,
336 EmitKind::Executable => Phase::Link,
337 }
338}
339
340fn extension(path: &str) -> &str {
342 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
343 match name.rfind('.') {
344 Some(0) | None => "",
346 Some(i) => &name[i + 1..],
347 }
348}
349
350fn stem(path: &str) -> &str {
353 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
354 match name.rfind('.') {
355 Some(0) | None => name,
356 Some(i) => &name[..i],
357 }
358}
359
360fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
362 match phase {
363 Phase::Preprocess => "i",
364 Phase::Compile => match opts.emit {
368 EmitKind::Tast => "tast",
369 EmitKind::Ir => "ir",
370 EmitKind::MirFinal => "mir",
371 EmitKind::SafetySummary => "safety.json",
375 EmitKind::TypeGranules => "granules.txt",
378 _ => "s",
379 },
380 Phase::Assemble => {
383 if opts.target.os == Os::Windows {
384 "obj"
385 } else {
386 "o"
387 }
388 }
389 Phase::Link => "",
390 }
391}
392
393fn default_exe(opts: &Options) -> &'static str {
395 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
396}
397
398impl Plan {
399 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
408 if inputs.is_empty() {
409 return Err(plan_err("no input files"));
410 }
411 let last = last_phase(opts.emit);
412 let linking = last == Phase::Link;
413
414 let mut kinds = Vec::with_capacity(inputs.len());
415 for input in inputs {
416 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
417 }
418
419 let producing = if linking {
424 0
425 } else {
426 kinds
427 .iter()
428 .filter(|k| **k != InputKind::LinkerInput)
429 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
430 .count()
431 };
432 if output.is_some() && !linking && producing > 1 {
433 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
434 }
435
436 let mut notes = Vec::new();
437 let mut jobs = Vec::with_capacity(inputs.len());
438 let mut link_inputs = Vec::new();
439
440 for (input, kind) in inputs.iter().zip(kinds) {
441 if kind == InputKind::LinkerInput {
446 if linking {
447 link_inputs.push(if input.library {
448 Item::Library(input.path.clone())
449 } else {
450 Item::File(input.path.clone())
451 });
452 } else {
453 notes.push(format!(
456 "{}: linker input unused because linking was not requested",
457 if input.library {
458 format!("-l{}", input.path)
459 } else {
460 input.path.clone()
461 }
462 ));
463 }
464 if input.library {
468 continue;
469 }
470 jobs.push(Job {
471 input: input.path.clone(),
472 kind,
473 phases: Vec::new(),
474 output: Output::File(input.path.clone()),
475 });
476 continue;
477 }
478
479 let phases: Vec<Phase> =
480 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
481 let Some(&final_phase) = phases.last() else {
485 notes.push(format!(
486 "{}: input unused because it enters the pipeline after the last phase \
487 the mode flags asked for",
488 input.path
489 ));
490 jobs.push(Job {
491 input: input.path.clone(),
492 kind,
493 phases,
494 output: Output::File(input.path.clone()),
495 });
496 continue;
497 };
498 let named = if producing == 1 { output } else { None };
499 let out = if final_phase == Phase::Link {
500 let ext = suffix_for(Phase::Assemble, opts);
502 Output::Temporary(format!("{}.{ext}", stem(&input.path)))
503 } else if let Some(o) = named {
504 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
509 } else if final_phase == Phase::Preprocess {
510 Output::Stdout
513 } else {
514 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
515 };
516 if let Output::File(path) = &out {
521 if *path == input.path {
522 return Err(plan_err(format!(
523 "input file `{}` is the same as the output file",
524 input.path
525 )));
526 }
527 }
528
529 if linking {
530 if let Some(p) = out.as_link_input() {
531 link_inputs.push(Item::File(p.to_owned()));
532 }
533 }
534 jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
535 }
536
537 let link = linking.then(|| LinkJob {
538 inputs: link_inputs,
539 output: output.unwrap_or(default_exe(opts)).to_owned(),
540 });
541
542 Ok(Plan { jobs, link, notes })
543 }
544
545 #[must_use]
551 pub fn render(&self) -> String {
552 let mut out = String::new();
553 for note in &self.notes {
554 let _ = writeln!(out, "note: {note}");
555 }
556 for job in &self.jobs {
557 if job.phases.is_empty() {
561 continue;
562 }
563 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
564 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
565 }
566 if let Some(link) = &self.link {
567 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
568 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
569 }
570 out
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use rucc_session::Options;
577
578 use super::*;
579
580 fn opts(triple: &str) -> Options {
581 Options::new(triple.parse().expect("test triple"))
582 }
583
584 fn linux() -> Options {
585 opts("x86_64-unknown-linux-gnu")
586 }
587
588 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
589 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
590 Plan::new(o, &inputs, output).expect("expected a plan")
591 }
592
593 #[test]
594 fn extensions_map_to_the_table_in_the_spec() {
595 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
596 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
597 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
598 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
599 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
600 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
601 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
602 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
603 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
604 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
605 }
606
607 #[test]
608 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
609 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
612 assert_eq!(InputKind::Ir.as_str(), "ir");
613 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
614 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
615 }
616
617 #[test]
618 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
619 let mut o = linux();
622 o.emit = EmitKind::Ir;
623 let inputs = [Input::new("a.ir")];
624 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
625 assert!(error.message.contains("is the same as the output file"), "{error}");
626 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
629 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
630 }
631
632 #[test]
633 fn capital_s_and_small_s_are_different_languages() {
634 let hi = InputKind::from_path("a.S").unwrap();
637 let lo = InputKind::from_path("a.s").unwrap();
638 assert_ne!(hi, lo);
639 assert!(hi.full_sequence().contains(&Phase::Preprocess));
640 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
641 }
642
643 #[test]
644 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
645 let e = InputKind::from_path("a.cpp").unwrap_err();
646 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
647 let e = InputKind::from_x_arg("c++").unwrap_err();
648 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
649 }
650
651 #[test]
652 fn a_file_with_no_extension_goes_to_the_linker() {
653 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
654 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
655 }
656
657 #[test]
658 fn the_default_line_compiles_and_links_to_a_out() {
659 let p = plan(&linux(), &["a.c"], None);
660 assert_eq!(
661 p.jobs[0].phases,
662 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
663 );
664 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
665 let link = p.link.expect("expected a link step");
666 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
667 assert_eq!(link.output, "a.out");
668 }
669
670 #[test]
671 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
672 let mut o = linux();
673 o.emit = EmitKind::Object;
674 let p = plan(&o, &["src/a.c", "src/b.c"], None);
675 assert!(p.link.is_none());
676 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
677 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
678 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
681 }
682
683 #[test]
684 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
685 let mut o = linux();
686 o.emit = EmitKind::Preprocessed;
687 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
688 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
689 }
690
691 #[test]
692 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
693 let mut o = linux();
694 o.emit = EmitKind::Preprocessed;
695 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
696 o.emit = EmitKind::Object;
697 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
698 let p = plan(&linux(), &["a.c"], Some("-"));
701 assert_eq!(p.link.expect("a link step").output, "-");
702 }
703
704 #[test]
705 fn dash_s_produces_assembly_named_after_the_source() {
706 let mut o = linux();
707 o.emit = EmitKind::Asm;
708 let p = plan(&o, &["dir/a.c"], None);
709 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
710 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
711 }
712
713 #[test]
714 fn an_already_preprocessed_file_skips_the_preprocessor() {
715 let p = plan(&linux(), &["a.i"], None);
716 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
717 }
718
719 #[test]
720 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
721 let p = plan(&linux(), &["a.S"], None);
722 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
723 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
724 }
725
726 #[test]
727 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
728 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
731 let link = p.link.expect("expected a link step");
732 assert_eq!(
733 link.inputs,
734 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
735 );
736 }
737
738 #[test]
739 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
740 let mut o = linux();
742 o.emit = EmitKind::Object;
743 let p = plan(&o, &["a.c", "b.o"], None);
744 assert!(p.jobs[1].phases.is_empty());
745 assert_eq!(p.notes.len(), 1);
746 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
747 }
748
749 #[test]
750 fn dash_o_with_several_compilations_is_rejected() {
751 let mut o = linux();
752 o.emit = EmitKind::Object;
753 let inputs = [Input::new("a.c"), Input::new("b.c")];
754 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
755 assert!(e.message.contains("multiple inputs"), "{}", e.message);
756 }
757
758 #[test]
759 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
760 let mut o = linux();
763 o.emit = EmitKind::Object;
764 let inputs = [Input::new("a.c"), Input::new("b.o")];
765 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
766 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
767 }
768
769 #[test]
770 fn dash_x_overrides_the_extension() {
771 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), library: false }];
772 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
773 assert_eq!(p.jobs[0].kind, InputKind::C);
774 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
775 }
776
777 #[test]
778 fn windows_gets_obj_and_a_exe() {
779 let o = opts("x86_64-pc-windows-msvc");
780 let p = plan(&o, &["a.c"], None);
781 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
782 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
783 }
784
785 #[test]
786 fn the_intermediate_dumps_stop_where_dash_s_stops() {
787 for emit in [
788 EmitKind::Tast,
789 EmitKind::Ir,
790 EmitKind::MirFinal,
791 EmitKind::SafetySummary,
792 EmitKind::TypeGranules,
793 ] {
794 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
795 }
796 }
797
798 #[test]
799 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
800 for (emit, name) in [
803 (EmitKind::Asm, "a.s"),
804 (EmitKind::Tast, "a.tast"),
805 (EmitKind::Ir, "a.ir"),
806 (EmitKind::MirFinal, "a.mir"),
807 (EmitKind::SafetySummary, "a.safety.json"),
808 (EmitKind::TypeGranules, "a.granules.txt"),
809 ] {
810 let mut o = linux();
811 o.emit = emit;
812 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
813 }
814 }
815
816 #[test]
817 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
818 let mut o = linux();
821 o.emit = EmitKind::Preprocessed;
822 let p = plan(&o, &["a.c", "b.s"], None);
823 assert!(p.jobs[1].phases.is_empty());
824 assert_eq!(p.notes.len(), 1);
825 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
826 let inputs = [Input::new("a.c"), Input::new("b.s")];
828 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
829 }
830
831 #[test]
832 fn no_inputs_is_an_error() {
833 assert!(Plan::new(&linux(), &[], None).is_err());
834 }
835
836 #[test]
837 fn the_rendering_says_what_will_happen() {
838 let p = plan(&linux(), &["a.c", "b.o"], None);
839 let text = p.render();
840 assert!(
841 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
842 "{text}"
843 );
844 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
845 assert_eq!(text.matches("b.o").count(), 1, "{text}");
847 }
848}