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 Assembler,
64 AssemblerWithCpp,
67 LinkerInput,
69}
70
71impl InputKind {
72 #[must_use]
74 pub fn as_str(self) -> &'static str {
75 match self {
76 InputKind::C => "c",
77 InputKind::CHeader => "c-header",
78 InputKind::PreprocessedC => "cpp-output",
79 InputKind::Assembler => "assembler",
80 InputKind::AssemblerWithCpp => "assembler-with-cpp",
81 InputKind::LinkerInput => "linker-input",
82 }
83 }
84
85 pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
92 match name {
93 "c" => Ok(InputKind::C),
94 "c-header" => Ok(InputKind::CHeader),
95 "cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
96 "assembler" => Ok(InputKind::Assembler),
97 "assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
98 "c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
99 Err(XError::Unsupported(name.to_owned()))
100 }
101 _ => Err(XError::Unknown(name.to_owned())),
102 }
103 }
104
105 pub fn from_path(path: &str) -> Result<InputKind, XError> {
116 let ext = extension(path);
117 match ext {
118 "c" => Ok(InputKind::C),
122 "i" => Ok(InputKind::PreprocessedC),
123 "h" => Ok(InputKind::CHeader),
124 "s" => Ok(InputKind::Assembler),
125 "S" | "sx" => Ok(InputKind::AssemblerWithCpp),
126 "cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
127 Err(XError::Unsupported(ext.to_owned()))
128 }
129 _ => Ok(InputKind::LinkerInput),
130 }
131 }
132
133 fn full_sequence(self) -> &'static [Phase] {
135 use Phase::{Assemble, Compile, Link, Preprocess};
136 match self {
137 InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
138 InputKind::PreprocessedC => &[Compile, Assemble, Link],
139 InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
142 InputKind::Assembler => &[Assemble, Link],
143 InputKind::LinkerInput => &[Link],
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum XError {
151 Unknown(String),
153 Unsupported(String),
155}
156
157impl std::fmt::Display for XError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 XError::Unknown(name) => {
161 write!(
162 f,
163 "unknown language `{name}`; \
164 accepted: c, c-header, cpp-output, assembler, assembler-with-cpp, none"
165 )
166 }
167 XError::Unsupported(name) => {
168 write!(
169 f,
170 "`{name}` is not C, and this compiler is only ever going to compile C; \
171 see the not-in-scope list in spec/00-README.md"
172 )
173 }
174 }
175 }
176}
177
178impl std::error::Error for XError {}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Input {
183 pub path: String,
185 pub forced: Option<InputKind>,
187}
188
189impl Input {
190 #[must_use]
192 pub fn new(path: impl Into<String>) -> Input {
193 Input { path: path.into(), forced: None }
194 }
195
196 pub fn kind(&self) -> Result<InputKind, XError> {
202 match self.forced {
203 Some(k) => Ok(k),
204 None => InputKind::from_path(&self.path),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum Output {
212 Stdout,
214 File(String),
216 Temporary(String),
219}
220
221impl Output {
222 fn render(&self) -> String {
223 match self {
224 Output::Stdout => "-".to_owned(),
225 Output::File(p) => p.clone(),
226 Output::Temporary(p) => format!("{p} (temporary)"),
227 }
228 }
229
230 fn as_link_input(&self) -> Option<&str> {
232 match self {
233 Output::File(p) | Output::Temporary(p) => Some(p),
234 Output::Stdout => None,
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct Job {
242 pub input: String,
244 pub kind: InputKind,
246 pub phases: Vec<Phase>,
248 pub output: Output,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct LinkJob {
255 pub inputs: Vec<String>,
257 pub output: String,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct Plan {
264 pub jobs: Vec<Job>,
266 pub link: Option<LinkJob>,
268 pub notes: Vec<String>,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct PlanError {
276 pub message: String,
278}
279
280impl std::fmt::Display for PlanError {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 f.write_str(&self.message)
283 }
284}
285
286impl std::error::Error for PlanError {}
287
288fn plan_err(message: impl Into<String>) -> PlanError {
289 PlanError { message: message.into() }
290}
291
292#[must_use]
297pub fn last_phase(emit: EmitKind) -> Phase {
298 match emit {
299 EmitKind::Preprocessed => Phase::Preprocess,
300 EmitKind::Asm | EmitKind::Tast | EmitKind::Ir | EmitKind::MirFinal => Phase::Compile,
301 EmitKind::Object => Phase::Assemble,
302 EmitKind::Executable => Phase::Link,
303 }
304}
305
306fn extension(path: &str) -> &str {
308 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
309 match name.rfind('.') {
310 Some(0) | None => "",
312 Some(i) => &name[i + 1..],
313 }
314}
315
316fn stem(path: &str) -> &str {
319 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
320 match name.rfind('.') {
321 Some(0) | None => name,
322 Some(i) => &name[..i],
323 }
324}
325
326fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
328 match phase {
329 Phase::Preprocess => "i",
330 Phase::Compile => match opts.emit {
334 EmitKind::Tast => "tast",
335 EmitKind::Ir => "ir",
336 EmitKind::MirFinal => "mir",
337 _ => "s",
338 },
339 Phase::Assemble => {
342 if opts.target.os == Os::Windows {
343 "obj"
344 } else {
345 "o"
346 }
347 }
348 Phase::Link => "",
349 }
350}
351
352fn default_exe(opts: &Options) -> &'static str {
354 if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
355}
356
357impl Plan {
358 pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
367 if inputs.is_empty() {
368 return Err(plan_err("no input files"));
369 }
370 let last = last_phase(opts.emit);
371 let linking = last == Phase::Link;
372
373 let mut kinds = Vec::with_capacity(inputs.len());
374 for input in inputs {
375 kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
376 }
377
378 let producing = if linking {
383 0
384 } else {
385 kinds
386 .iter()
387 .filter(|k| **k != InputKind::LinkerInput)
388 .filter(|k| k.full_sequence().iter().any(|p| *p <= last))
389 .count()
390 };
391 if output.is_some() && !linking && producing > 1 {
392 return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
393 }
394
395 let mut notes = Vec::new();
396 let mut jobs = Vec::with_capacity(inputs.len());
397 let mut link_inputs = Vec::new();
398
399 for (input, kind) in inputs.iter().zip(kinds) {
400 if kind == InputKind::LinkerInput {
405 if linking {
406 link_inputs.push(input.path.clone());
407 } else {
408 notes.push(format!(
411 "{}: linker input unused because linking was not requested",
412 input.path
413 ));
414 }
415 jobs.push(Job {
416 input: input.path.clone(),
417 kind,
418 phases: Vec::new(),
419 output: Output::File(input.path.clone()),
420 });
421 continue;
422 }
423
424 let phases: Vec<Phase> =
425 kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
426 let Some(&final_phase) = phases.last() else {
430 notes.push(format!(
431 "{}: input unused because it enters the pipeline after the last phase \
432 the mode flags asked for",
433 input.path
434 ));
435 jobs.push(Job {
436 input: input.path.clone(),
437 kind,
438 phases,
439 output: Output::File(input.path.clone()),
440 });
441 continue;
442 };
443 let named = if producing == 1 { output } else { None };
444 let out = if final_phase == Phase::Link {
445 let ext = suffix_for(Phase::Assemble, opts);
447 Output::Temporary(format!("{}.{ext}", stem(&input.path)))
448 } else if let Some(o) = named {
449 if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
454 } else if final_phase == Phase::Preprocess {
455 Output::Stdout
458 } else {
459 Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
460 };
461
462 if linking {
463 if let Some(p) = out.as_link_input() {
464 link_inputs.push(p.to_owned());
465 }
466 }
467 jobs.push(Job { input: input.path.clone(), kind, phases, output: out });
468 }
469
470 let link = linking.then(|| LinkJob {
471 inputs: link_inputs,
472 output: output.unwrap_or(default_exe(opts)).to_owned(),
473 });
474
475 Ok(Plan { jobs, link, notes })
476 }
477
478 #[must_use]
484 pub fn render(&self) -> String {
485 let mut out = String::new();
486 for note in &self.notes {
487 let _ = writeln!(out, "note: {note}");
488 }
489 for job in &self.jobs {
490 if job.phases.is_empty() {
494 continue;
495 }
496 let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
497 let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
498 }
499 if let Some(link) = &self.link {
500 let _ = writeln!(out, "link: {} -> {}", link.inputs.join(" "), link.output);
501 }
502 out
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use rucc_session::Options;
509
510 use super::*;
511
512 fn opts(triple: &str) -> Options {
513 Options::new(triple.parse().expect("test triple"))
514 }
515
516 fn linux() -> Options {
517 opts("x86_64-unknown-linux-gnu")
518 }
519
520 fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
521 let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
522 Plan::new(o, &inputs, output).expect("expected a plan")
523 }
524
525 #[test]
526 fn extensions_map_to_the_table_in_the_spec() {
527 assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
528 assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
529 assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
530 assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
531 assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
532 assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
533 assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
534 assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
535 assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
536 }
537
538 #[test]
539 fn capital_s_and_small_s_are_different_languages() {
540 let hi = InputKind::from_path("a.S").unwrap();
543 let lo = InputKind::from_path("a.s").unwrap();
544 assert_ne!(hi, lo);
545 assert!(hi.full_sequence().contains(&Phase::Preprocess));
546 assert!(!lo.full_sequence().contains(&Phase::Preprocess));
547 }
548
549 #[test]
550 fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
551 let e = InputKind::from_path("a.cpp").unwrap_err();
552 assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
553 let e = InputKind::from_x_arg("c++").unwrap_err();
554 assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
555 }
556
557 #[test]
558 fn a_file_with_no_extension_goes_to_the_linker() {
559 assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
560 assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
561 }
562
563 #[test]
564 fn the_default_line_compiles_and_links_to_a_out() {
565 let p = plan(&linux(), &["a.c"], None);
566 assert_eq!(
567 p.jobs[0].phases,
568 vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
569 );
570 assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
571 let link = p.link.expect("expected a link step");
572 assert_eq!(link.inputs, vec!["a.o"]);
573 assert_eq!(link.output, "a.out");
574 }
575
576 #[test]
577 fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
578 let mut o = linux();
579 o.emit = EmitKind::Object;
580 let p = plan(&o, &["src/a.c", "src/b.c"], None);
581 assert!(p.link.is_none());
582 assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
583 assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
584 assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
587 }
588
589 #[test]
590 fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
591 let mut o = linux();
592 o.emit = EmitKind::Preprocessed;
593 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
594 assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
595 }
596
597 #[test]
598 fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
599 let mut o = linux();
600 o.emit = EmitKind::Preprocessed;
601 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
602 o.emit = EmitKind::Object;
603 assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
604 let p = plan(&linux(), &["a.c"], Some("-"));
607 assert_eq!(p.link.expect("a link step").output, "-");
608 }
609
610 #[test]
611 fn dash_s_produces_assembly_named_after_the_source() {
612 let mut o = linux();
613 o.emit = EmitKind::Asm;
614 let p = plan(&o, &["dir/a.c"], None);
615 assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
616 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
617 }
618
619 #[test]
620 fn an_already_preprocessed_file_skips_the_preprocessor() {
621 let p = plan(&linux(), &["a.i"], None);
622 assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
623 }
624
625 #[test]
626 fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
627 let p = plan(&linux(), &["a.S"], None);
628 assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
629 assert!(!p.jobs[0].phases.contains(&Phase::Compile));
630 }
631
632 #[test]
633 fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
634 let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
637 let link = p.link.expect("expected a link step");
638 assert_eq!(link.inputs, vec!["a.o", "b.o", "libm.a"]);
639 }
640
641 #[test]
642 fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
643 let mut o = linux();
645 o.emit = EmitKind::Object;
646 let p = plan(&o, &["a.c", "b.o"], None);
647 assert!(p.jobs[1].phases.is_empty());
648 assert_eq!(p.notes.len(), 1);
649 assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
650 }
651
652 #[test]
653 fn dash_o_with_several_compilations_is_rejected() {
654 let mut o = linux();
655 o.emit = EmitKind::Object;
656 let inputs = [Input::new("a.c"), Input::new("b.c")];
657 let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
658 assert!(e.message.contains("multiple inputs"), "{}", e.message);
659 }
660
661 #[test]
662 fn dash_o_with_one_compilation_and_some_objects_is_fine() {
663 let mut o = linux();
666 o.emit = EmitKind::Object;
667 let inputs = [Input::new("a.c"), Input::new("b.o")];
668 let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
669 assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
670 }
671
672 #[test]
673 fn dash_x_overrides_the_extension() {
674 let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C) }];
675 let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
676 assert_eq!(p.jobs[0].kind, InputKind::C);
677 assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
678 }
679
680 #[test]
681 fn windows_gets_obj_and_a_exe() {
682 let o = opts("x86_64-pc-windows-msvc");
683 let p = plan(&o, &["a.c"], None);
684 assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
685 assert_eq!(p.link.expect("expected a link step").output, "a.exe");
686 }
687
688 #[test]
689 fn the_intermediate_dumps_stop_where_dash_s_stops() {
690 for emit in [EmitKind::Tast, EmitKind::Ir, EmitKind::MirFinal] {
691 assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
692 }
693 }
694
695 #[test]
696 fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
697 for (emit, name) in [
700 (EmitKind::Asm, "a.s"),
701 (EmitKind::Tast, "a.tast"),
702 (EmitKind::Ir, "a.ir"),
703 (EmitKind::MirFinal, "a.mir"),
704 ] {
705 let mut o = linux();
706 o.emit = emit;
707 assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
708 }
709 }
710
711 #[test]
712 fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
713 let mut o = linux();
716 o.emit = EmitKind::Preprocessed;
717 let p = plan(&o, &["a.c", "b.s"], None);
718 assert!(p.jobs[1].phases.is_empty());
719 assert_eq!(p.notes.len(), 1);
720 assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
721 let inputs = [Input::new("a.c"), Input::new("b.s")];
723 assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
724 }
725
726 #[test]
727 fn no_inputs_is_an_error() {
728 assert!(Plan::new(&linux(), &[], None).is_err());
729 }
730
731 #[test]
732 fn the_rendering_says_what_will_happen() {
733 let p = plan(&linux(), &["a.c", "b.o"], None);
734 let text = p.render();
735 assert!(
736 text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
737 "{text}"
738 );
739 assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
740 assert_eq!(text.matches("b.o").count(), 1, "{text}");
742 }
743}