1use std::fmt::Write as _;
12
13use rucc_session::{EmitKind, Options};
14use rucc_target::Os;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum Phase {
24 Preprocess,
26 Compile,
28 Assemble,
30 Link,
32}
33
34impl Phase {
35 #[must_use]
37 pub fn as_str(self) -> &'static str {
38 match self {
39 Phase::Preprocess => "preprocess",
40 Phase::Compile => "compile",
41 Phase::Assemble => "assemble",
42 Phase::Link => "link",
43 }
44 }
45}
46
47impl std::fmt::Display for Phase {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.write_str(self.as_str())
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum InputKind {
56 C,
58 CHeader,
60 PreprocessedC,
62 Ir,
70 Assembler,
72 AssemblerWithCpp,
75 LinkerInput,
77}
78
79impl InputKind {
80 #[must_use]
82 pub fn as_str(self) -> &'static str {
83 match self {
84 InputKind::C => "c",
85 InputKind::CHeader => "c-header",
86 InputKind::PreprocessedC => "cpp-output",
87 InputKind::Ir => "ir",
88 InputKind::Assembler => "assembler",
89 InputKind::AssemblerWithCpp => "assembler-with-cpp",
90 InputKind::LinkerInput => "linker-input",
91 }
92 }
93
94 pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
101 match name {
102 "c" => Ok(InputKind::C),
103 "c-header" => Ok(InputKind::CHeader),
104 "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
105 "ir" => Ok(InputKind::Ir),
106 "assembler" => Ok(InputKind::Assembler),
107 "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
108 "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
109 Err(XError::Unsupported(name.to_owned()))
110 }
111 _ => Err(XError::Unknown(name.to_owned())),
112 }
113 }
114
115 pub fn from_path(path: &str) -> Result<InputKind, XError> {
126 let ext = extension(path);
127 match ext {
128 "c" => Ok(InputKind::C),
132 "i" => Ok(InputKind::PreprocessedC),
133 "ir" => Ok(InputKind::Ir),
134 "h" => Ok(InputKind::CHeader),
135 "s" => Ok(InputKind::Assembler),
136 "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
137 "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
138 Err(XError::Unsupported(ext.to_owned()))
139 }
140 _ => Ok(InputKind::LinkerInput),
141 }
142 }
143
144 fn full_sequence(self) -> &'static [Phase] {
146 use Phase::{Assemble, Compile, Link, Preprocess};
147 match self {
148 InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
149 InputKind::PreprocessedC | InputKind::Ir => &[Compile, Assemble, Link],
150 InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
153 InputKind::Assembler => &[Assemble, Link],
154 InputKind::LinkerInput => &[Link],
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum XError {
162 Unknown(String),
164 Unsupported(String),
166}
167
168impl std::fmt::Display for XError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 XError::Unknown(name) => {
172 write!(
173 f,
174 "unknown language `{name}`; \
175 accepted: c, c-header, cpp-output, ir, assembler, assembler-with-cpp, none"
176 )
177 }
178 XError::Unsupported(name) => {
179 write!(
180 f,
181 "`{name}` is not C, and this compiler is only ever going to compile C; \
182 see the not-in-scope list in spec/00-README.md"
183 )
184 }
185 }
186 }
187}
188
189impl std::error::Error for XError {}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Input {
194 pub path: String,
196 pub forced: Option<InputKind>,
198}
199
200impl Input {
201 #[must_use]
203 pub fn new(path: impl Into<String>) -> Input {
204 Input { path: path.into(), forced: None }
205 }
206
207 pub fn kind(&self) -> Result<InputKind, XError> {
213 match self.forced {
214 Some(k) => Ok(k),
215 None => InputKind::from_path(&self.path),
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum Output {
223 Stdout,
225 File(String),
227 Temporary(String),
230}
231
232impl Output {
233 fn render(&self) -> String {
234 match self {
235 Output::Stdout => "-".to_owned(),
236 Output::File(p) => p.clone(),
237 Output::Temporary(p) => format!("{p} (temporary)"),
238 }
239 }
240
241 fn as_link_input(&self) -> Option<&str> {
243 match self {
244 Output::File(p) | Output::Temporary(p) => Some(p),
245 Output::Stdout => None,
246 }
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct Job {
253 pub input: String,
255 pub kind: InputKind,
257 pub phases: Vec<Phase>,
259 pub output: Output,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct LinkJob {
266 pub inputs: Vec<String>,
268 pub output: String,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct Plan {
275 pub jobs: Vec<Job>,
277 pub link: Option<LinkJob>,
279 pub notes: Vec<String>,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct PlanError {
287 pub message: String,
289}
290
291impl std::fmt::Display for PlanError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 f.write_str(&self.message)
294 }
295}
296
297impl std::error::Error for PlanError {}
298
299fn plan_err(message: impl Into<String>) -> PlanError {
300 PlanError { message: message.into() }
301}
302
303#[must_use]
308pub fn last_phase(emit: EmitKind) -> Phase {
309 match emit {
310 EmitKind::Preprocessed => Phase::Preprocess,
311 EmitKind::Asm | EmitKind::Tast | EmitKind::Ir | EmitKind::MirFinal => Phase::Compile,
312 EmitKind::Object => Phase::Assemble,
313 EmitKind::Executable => Phase::Link,
314 }
315}
316
317fn extension(path: &str) -> &str {
319 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
320 match name.rfind('.') {
321 Some(0) | None => "",
323 Some(i) => &name[i + 1..],
324 }
325}
326
327fn stem(path: &str) -> &str {
330 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
331 match name.rfind('.') {
332 Some(0) | None => name,
333 Some(i) => &name[..i],
334 }
335}
336
337fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
339 match phase {
340 Phase::Preprocess => "i",
341 Phase::Compile => match opts.emit {
345 EmitKind::Tast => "tast",
346 EmitKind::Ir => "ir",
347 EmitKind::MirFinal => "mir",
348 _ => "s",
349 },
350 Phase::Assemble => {
353 if opts.target.os == Os::Windows {
354 "obj"
355 } else {
356 "o"
357 }
358 }
359 Phase::Link => "",
360 }
361}
362
363fn default_exe(opts: &Options) -> &'static str {
365 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
366}
367
368impl Plan {
369 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
378 if inputs.is_empty() {
379 return Err(plan_err("no input files"));
380 }
381 let last = last_phase(opts.emit);
382 let linking = last == Phase::Link;
383
384 let mut kinds = Vec::with_capacity(inputs.len());
385 for input in inputs {
386 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
387 }
388
389 let producing = if linking {
394 0
395 } else {
396 kinds
397 .iter()
398 .filter(|k| **k != InputKind::LinkerInput)
399 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
400 .count()
401 };
402 if output.is_some() && !linking && producing > 1 {
403 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
404 }
405
406 let mut notes = Vec::new();
407 let mut jobs = Vec::with_capacity(inputs.len());
408 let mut link_inputs = Vec::new();
409
410 for (input, kind) in inputs.iter().zip(kinds) {
411 if kind == InputKind::LinkerInput {
416 if linking {
417 link_inputs.push(input.path.clone());
418 } else {
419 notes.push(format!(
422 "{}: linker input unused because linking was not requested",
423 input.path
424 ));
425 }
426 jobs.push(Job {
427 input: input.path.clone(),
428 kind,
429 phases: Vec::new(),
430 output: Output::File(input.path.clone()),
431 });
432 continue;
433 }
434
435 let phases: Vec<Phase> =
436 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
437 let Some(&final_phase) = phases.last() else {
441 notes.push(format!(
442 "{}: input unused because it enters the pipeline after the last phase \
443 the mode flags asked for",
444 input.path
445 ));
446 jobs.push(Job {
447 input: input.path.clone(),
448 kind,
449 phases,
450 output: Output::File(input.path.clone()),
451 });
452 continue;
453 };
454 let named = if producing == 1 { output } else { None };
455 let out = if final_phase == Phase::Link {
456 let ext = suffix_for(Phase::Assemble, opts);
458 Output::Temporary(format!("{}.{ext}", stem(&input.path)))
459 } else if let Some(o) = named {
460 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
465 } else if final_phase == Phase::Preprocess {
466 Output::Stdout
469 } else {
470 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
471 };
472 if let Output::File(path) = &out {
477 if *path == input.path {
478 return Err(plan_err(format!(
479 "input file `{}` is the same as the output file",
480 input.path
481 )));
482 }
483 }
484
485 if linking {
486 if let Some(p) = out.as_link_input() {
487 link_inputs.push(p.to_owned());
488 }
489 }
490 jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
491 }
492
493 let link = linking.then(|| LinkJob {
494 inputs: link_inputs,
495 output: output.unwrap_or(default_exe(opts)).to_owned(),
496 });
497
498 Ok(Plan { jobs, link, notes })
499 }
500
501 #[must_use]
507 pub fn render(&self) -> String {
508 let mut out = String::new();
509 for note in &self.notes {
510 let _ = writeln!(out, "note: {note}");
511 }
512 for job in &self.jobs {
513 if job.phases.is_empty() {
517 continue;
518 }
519 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
520 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
521 }
522 if let Some(link) = &self.link {
523 let _ = writeln!(out, "link: {} -> {}", link.inputs.join(" "), link.output);
524 }
525 out
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use rucc_session::Options;
532
533 use super::*;
534
535 fn opts(triple: &str) -> Options {
536 Options::new(triple.parse().expect("test triple"))
537 }
538
539 fn linux() -> Options {
540 opts("x86_64-unknown-linux-gnu")
541 }
542
543 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
544 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
545 Plan::new(o, &inputs, output).expect("expected a plan")
546 }
547
548 #[test]
549 fn extensions_map_to_the_table_in_the_spec() {
550 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
551 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
552 assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
553 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
554 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
555 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
556 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
557 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
558 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
559 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
560 }
561
562 #[test]
563 fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
564 assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
567 assert_eq!(InputKind::Ir.as_str(), "ir");
568 assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
569 assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
570 }
571
572 #[test]
573 fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
574 let mut o = linux();
577 o.emit = EmitKind::Ir;
578 let inputs = [Input::new("a.ir")];
579 let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
580 assert!(error.message.contains("is the same as the output file"), "{error}");
581 assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
584 assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
585 }
586
587 #[test]
588 fn capital_s_and_small_s_are_different_languages() {
589 let hi = InputKind::from_path("a.S").unwrap();
592 let lo = InputKind::from_path("a.s").unwrap();
593 assert_ne!(hi, lo);
594 assert!(hi.full_sequence().contains(&Phase::Preprocess));
595 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
596 }
597
598 #[test]
599 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
600 let e = InputKind::from_path("a.cpp").unwrap_err();
601 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
602 let e = InputKind::from_x_arg("c++").unwrap_err();
603 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
604 }
605
606 #[test]
607 fn a_file_with_no_extension_goes_to_the_linker() {
608 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
609 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
610 }
611
612 #[test]
613 fn the_default_line_compiles_and_links_to_a_out() {
614 let p = plan(&linux(), &["a.c"], None);
615 assert_eq!(
616 p.jobs[0].phases,
617 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
618 );
619 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
620 let link = p.link.expect("expected a link step");
621 assert_eq!(link.inputs, vec!["a.o"]);
622 assert_eq!(link.output, "a.out");
623 }
624
625 #[test]
626 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
627 let mut o = linux();
628 o.emit = EmitKind::Object;
629 let p = plan(&o, &["src/a.c", "src/b.c"], None);
630 assert!(p.link.is_none());
631 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
632 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
633 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
636 }
637
638 #[test]
639 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
640 let mut o = linux();
641 o.emit = EmitKind::Preprocessed;
642 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
643 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
644 }
645
646 #[test]
647 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
648 let mut o = linux();
649 o.emit = EmitKind::Preprocessed;
650 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
651 o.emit = EmitKind::Object;
652 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
653 let p = plan(&linux(), &["a.c"], Some("-"));
656 assert_eq!(p.link.expect("a link step").output, "-");
657 }
658
659 #[test]
660 fn dash_s_produces_assembly_named_after_the_source() {
661 let mut o = linux();
662 o.emit = EmitKind::Asm;
663 let p = plan(&o, &["dir/a.c"], None);
664 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
665 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
666 }
667
668 #[test]
669 fn an_already_preprocessed_file_skips_the_preprocessor() {
670 let p = plan(&linux(), &["a.i"], None);
671 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
672 }
673
674 #[test]
675 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
676 let p = plan(&linux(), &["a.S"], None);
677 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
678 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
679 }
680
681 #[test]
682 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
683 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
686 let link = p.link.expect("expected a link step");
687 assert_eq!(link.inputs, vec!["a.o", "b.o", "libm.a"]);
688 }
689
690 #[test]
691 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
692 let mut o = linux();
694 o.emit = EmitKind::Object;
695 let p = plan(&o, &["a.c", "b.o"], None);
696 assert!(p.jobs[1].phases.is_empty());
697 assert_eq!(p.notes.len(), 1);
698 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
699 }
700
701 #[test]
702 fn dash_o_with_several_compilations_is_rejected() {
703 let mut o = linux();
704 o.emit = EmitKind::Object;
705 let inputs = [Input::new("a.c"), Input::new("b.c")];
706 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
707 assert!(e.message.contains("multiple inputs"), "{}", e.message);
708 }
709
710 #[test]
711 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
712 let mut o = linux();
715 o.emit = EmitKind::Object;
716 let inputs = [Input::new("a.c"), Input::new("b.o")];
717 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
718 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
719 }
720
721 #[test]
722 fn dash_x_overrides_the_extension() {
723 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C) }];
724 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
725 assert_eq!(p.jobs[0].kind, InputKind::C);
726 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
727 }
728
729 #[test]
730 fn windows_gets_obj_and_a_exe() {
731 let o = opts("x86_64-pc-windows-msvc");
732 let p = plan(&o, &["a.c"], None);
733 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
734 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
735 }
736
737 #[test]
738 fn the_intermediate_dumps_stop_where_dash_s_stops() {
739 for emit in [EmitKind::Tast, EmitKind::Ir, EmitKind::MirFinal] {
740 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
741 }
742 }
743
744 #[test]
745 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
746 for (emit, name) in [
749 (EmitKind::Asm, "a.s"),
750 (EmitKind::Tast, "a.tast"),
751 (EmitKind::Ir, "a.ir"),
752 (EmitKind::MirFinal, "a.mir"),
753 ] {
754 let mut o = linux();
755 o.emit = emit;
756 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
757 }
758 }
759
760 #[test]
761 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
762 let mut o = linux();
765 o.emit = EmitKind::Preprocessed;
766 let p = plan(&o, &["a.c", "b.s"], None);
767 assert!(p.jobs[1].phases.is_empty());
768 assert_eq!(p.notes.len(), 1);
769 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
770 let inputs = [Input::new("a.c"), Input::new("b.s")];
772 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
773 }
774
775 #[test]
776 fn no_inputs_is_an_error() {
777 assert!(Plan::new(&linux(), &[], None).is_err());
778 }
779
780 #[test]
781 fn the_rendering_says_what_will_happen() {
782 let p = plan(&linux(), &["a.c", "b.o"], None);
783 let text = p.render();
784 assert!(
785 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
786 "{text}"
787 );
788 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
789 assert_eq!(text.matches("b.o").count(), 1, "{text}");
791 }
792}