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 | EmitKind::Tast | EmitKind::Ir | EmitKind::MirFinal => Phase::Compile,
330 EmitKind::Object => Phase::Assemble,
331 EmitKind::Executable => Phase::Link,
332 }
333}
334
335fn extension(path: &str) -> &str {
337 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
338 match name.rfind('.') {
339 Some(0) | None => "",
341 Some(i) => &name[i + 1..],
342 }
343}
344
345fn stem(path: &str) -> &str {
348 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
349 match name.rfind('.') {
350 Some(0) | None => name,
351 Some(i) => &name[..i],
352 }
353}
354
355fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
357 match phase {
358 Phase::Preprocess => "i",
359 Phase::Compile => match opts.emit {
363 EmitKind::Tast => "tast",
364 EmitKind::Ir => "ir",
365 EmitKind::MirFinal => "mir",
366 _ => "s",
367 },
368 Phase::Assemble => {
371 if opts.target.os == Os::Windows {
372 "obj"
373 } else {
374 "o"
375 }
376 }
377 Phase::Link => "",
378 }
379}
380
381fn default_exe(opts: &Options) -> &'static str {
383 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
384}
385
386impl Plan {
387 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
396 if inputs.is_empty() {
397 return Err(plan_err("no input files"));
398 }
399 let last = last_phase(opts.emit);
400 let linking = last == Phase::Link;
401
402 let mut kinds = Vec::with_capacity(inputs.len());
403 for input in inputs {
404 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
405 }
406
407 let producing = if linking {
412 0
413 } else {
414 kinds
415 .iter()
416 .filter(|k| **k != InputKind::LinkerInput)
417 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
418 .count()
419 };
420 if output.is_some() && !linking && producing > 1 {
421 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
422 }
423
424 let mut notes = Vec::new();
425 let mut jobs = Vec::with_capacity(inputs.len());
426 let mut link_inputs = Vec::new();
427
428 for (input, kind) in inputs.iter().zip(kinds) {
429 if kind == InputKind::LinkerInput {
434 if linking {
435 link_inputs.push(if input.library {
436 Item::Library(input.path.clone())
437 } else {
438 Item::File(input.path.clone())
439 });
440 } else {
441 notes.push(format!(
444 "{}: linker input unused because linking was not requested",
445 if input.library {
446 format!("-l{}", input.path)
447 } else {
448 input.path.clone()
449 }
450 ));
451 }
452 if input.library {
456 continue;
457 }
458 jobs.push(Job {
459 input: input.path.clone(),
460 kind,
461 phases: Vec::new(),
462 output: Output::File(input.path.clone()),
463 });
464 continue;
465 }
466
467 let phases: Vec<Phase> =
468 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
469 let Some(&final_phase) = phases.last() else {
473 notes.push(format!(
474 "{}: input unused because it enters the pipeline after the last phase \
475 the mode flags asked for",
476 input.path
477 ));
478 jobs.push(Job {
479 input: input.path.clone(),
480 kind,
481 phases,
482 output: Output::File(input.path.clone()),
483 });
484 continue;
485 };
486 let named = if producing == 1 { output } else { None };
487 let out = if final_phase == Phase::Link {
488 let ext = suffix_for(Phase::Assemble, opts);
490 Output::Temporary(format!("{}.{ext}", stem(&input.path)))
491 } else if let Some(o) = named {
492 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
497 } else if final_phase == Phase::Preprocess {
498 Output::Stdout
501 } else {
502 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
503 };
504 if let Output::File(path) = &out {
509 if *path == input.path {
510 return Err(plan_err(format!(
511 "input file `{}` is the same as the output file",
512 input.path
513 )));
514 }
515 }
516
517 if linking {
518 if let Some(p) = out.as_link_input() {
519 link_inputs.push(Item::File(p.to_owned()));
520 }
521 }
522 jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
523 }
524
525 let link = linking.then(|| LinkJob {
526 inputs: link_inputs,
527 output: output.unwrap_or(default_exe(opts)).to_owned(),
528 });
529
530 Ok(Plan { jobs, link, notes })
531 }
532
533 #[must_use]
539 pub fn render(&self) -> String {
540 let mut out = String::new();
541 for note in &self.notes {
542 let _ = writeln!(out, "note: {note}");
543 }
544 for job in &self.jobs {
545 if job.phases.is_empty() {
549 continue;
550 }
551 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
552 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
553 }
554 if let Some(link) = &self.link {
555 let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
556 let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
557 }
558 out
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use rucc_session::Options;
565
566 use super::*;
567
568 fn opts(triple: &str) -> Options {
569 Options::new(triple.parse().expect("test triple"))
570 }
571
572 fn linux() -> Options {
573 opts("x86_64-unknown-linux-gnu")
574 }
575
576 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
577 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
578 Plan::new(o, &inputs, output).expect("expected a plan")
579 }
580
581 #[test]
582 fn extensions_map_to_the_table_in_the_spec() {
583 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
584 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
585 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
586 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
587 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
588 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
589 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
590 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
591 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
592 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
593 }
594
595 #[test]
596 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
597 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
600 assert_eq!(InputKind::Ir.as_str(), "ir");
601 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
602 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
603 }
604
605 #[test]
606 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
607 let mut o = linux();
610 o.emit = EmitKind::Ir;
611 let inputs = [Input::new("a.ir")];
612 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
613 assert!(error.message.contains("is the same as the output file"), "{error}");
614 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
617 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
618 }
619
620 #[test]
621 fn capital_s_and_small_s_are_different_languages() {
622 let hi = InputKind::from_path("a.S").unwrap();
625 let lo = InputKind::from_path("a.s").unwrap();
626 assert_ne!(hi, lo);
627 assert!(hi.full_sequence().contains(&Phase::Preprocess));
628 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
629 }
630
631 #[test]
632 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
633 let e = InputKind::from_path("a.cpp").unwrap_err();
634 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
635 let e = InputKind::from_x_arg("c++").unwrap_err();
636 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
637 }
638
639 #[test]
640 fn a_file_with_no_extension_goes_to_the_linker() {
641 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
642 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
643 }
644
645 #[test]
646 fn the_default_line_compiles_and_links_to_a_out() {
647 let p = plan(&linux(), &["a.c"], None);
648 assert_eq!(
649 p.jobs[0].phases,
650 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
651 );
652 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
653 let link = p.link.expect("expected a link step");
654 assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
655 assert_eq!(link.output, "a.out");
656 }
657
658 #[test]
659 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
660 let mut o = linux();
661 o.emit = EmitKind::Object;
662 let p = plan(&o, &["src/a.c", "src/b.c"], None);
663 assert!(p.link.is_none());
664 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
665 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
666 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
669 }
670
671 #[test]
672 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
673 let mut o = linux();
674 o.emit = EmitKind::Preprocessed;
675 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
676 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
677 }
678
679 #[test]
680 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
681 let mut o = linux();
682 o.emit = EmitKind::Preprocessed;
683 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
684 o.emit = EmitKind::Object;
685 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
686 let p = plan(&linux(), &["a.c"], Some("-"));
689 assert_eq!(p.link.expect("a link step").output, "-");
690 }
691
692 #[test]
693 fn dash_s_produces_assembly_named_after_the_source() {
694 let mut o = linux();
695 o.emit = EmitKind::Asm;
696 let p = plan(&o, &["dir/a.c"], None);
697 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
698 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
699 }
700
701 #[test]
702 fn an_already_preprocessed_file_skips_the_preprocessor() {
703 let p = plan(&linux(), &["a.i"], None);
704 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
705 }
706
707 #[test]
708 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
709 let p = plan(&linux(), &["a.S"], None);
710 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
711 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
712 }
713
714 #[test]
715 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
716 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
719 let link = p.link.expect("expected a link step");
720 assert_eq!(
721 link.inputs,
722 vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
723 );
724 }
725
726 #[test]
727 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
728 let mut o = linux();
730 o.emit = EmitKind::Object;
731 let p = plan(&o, &["a.c", "b.o"], None);
732 assert!(p.jobs[1].phases.is_empty());
733 assert_eq!(p.notes.len(), 1);
734 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
735 }
736
737 #[test]
738 fn dash_o_with_several_compilations_is_rejected() {
739 let mut o = linux();
740 o.emit = EmitKind::Object;
741 let inputs = [Input::new("a.c"), Input::new("b.c")];
742 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
743 assert!(e.message.contains("multiple inputs"), "{}", e.message);
744 }
745
746 #[test]
747 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
748 let mut o = linux();
751 o.emit = EmitKind::Object;
752 let inputs = [Input::new("a.c"), Input::new("b.o")];
753 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
754 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
755 }
756
757 #[test]
758 fn dash_x_overrides_the_extension() {
759 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), library: false }];
760 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
761 assert_eq!(p.jobs[0].kind, InputKind::C);
762 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
763 }
764
765 #[test]
766 fn windows_gets_obj_and_a_exe() {
767 let o = opts("x86_64-pc-windows-msvc");
768 let p = plan(&o, &["a.c"], None);
769 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
770 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
771 }
772
773 #[test]
774 fn the_intermediate_dumps_stop_where_dash_s_stops() {
775 for emit in [EmitKind::Tast, EmitKind::Ir, EmitKind::MirFinal] {
776 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
777 }
778 }
779
780 #[test]
781 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
782 for (emit, name) in [
785 (EmitKind::Asm, "a.s"),
786 (EmitKind::Tast, "a.tast"),
787 (EmitKind::Ir, "a.ir"),
788 (EmitKind::MirFinal, "a.mir"),
789 ] {
790 let mut o = linux();
791 o.emit = emit;
792 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
793 }
794 }
795
796 #[test]
797 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
798 let mut o = linux();
801 o.emit = EmitKind::Preprocessed;
802 let p = plan(&o, &["a.c", "b.s"], None);
803 assert!(p.jobs[1].phases.is_empty());
804 assert_eq!(p.notes.len(), 1);
805 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
806 let inputs = [Input::new("a.c"), Input::new("b.s")];
808 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
809 }
810
811 #[test]
812 fn no_inputs_is_an_error() {
813 assert!(Plan::new(&linux(), &[], None).is_err());
814 }
815
816 #[test]
817 fn the_rendering_says_what_will_happen() {
818 let p = plan(&linux(), &["a.c", "b.o"], None);
819 let text = p.render();
820 assert!(
821 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
822 "{text}"
823 );
824 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
825 assert_eq!(text.matches("b.o").count(), 1, "{text}");
827 }
828}