use std::fmt::Write as _;
use rucc_session::{EmitKind, Options, SaveTemps};
use rucc_target::Os;
use crate::link::Item;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Phase {
Preprocess,
Compile,
Assemble,
Archive,
Link,
}
impl Phase {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Phase::Preprocess => "preprocess",
Phase::Compile => "compile",
Phase::Assemble => "assemble",
Phase::Archive => "archive",
Phase::Link => "link",
}
}
}
impl std::fmt::Display for Phase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InputKind {
C,
CHeader,
PreprocessedC,
Ir,
Assembler,
AssemblerWithCpp,
LinkerInput,
}
impl InputKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
InputKind::C => "c",
InputKind::CHeader => "c-header",
InputKind::PreprocessedC => "cpp-output",
InputKind::Ir => "ir",
InputKind::Assembler => "assembler",
InputKind::AssemblerWithCpp => "assembler-with-cpp",
InputKind::LinkerInput => "linker-input",
}
}
pub fn from_x_arg(name: &str) -> Result<InputKind, XError> {
match name {
"c" => Ok(InputKind::C),
"c-header" => Ok(InputKind::CHeader),
"cpp-output" | "c-cpp-output" => Ok(InputKind::PreprocessedC),
"ir" => Ok(InputKind::Ir),
"assembler" => Ok(InputKind::Assembler),
"assembler-with-cpp" => Ok(InputKind::AssemblerWithCpp),
"c++" | "c++-header" | "c++-cpp-output" | "objective-c" | "objective-c++" => {
Err(XError::Unsupported(name.to_owned()))
}
_ => Err(XError::Unknown(name.to_owned())),
}
}
pub fn from_path(path: &str) -> Result<InputKind, XError> {
let ext = extension(path);
match ext {
"c" => Ok(InputKind::C),
"i" => Ok(InputKind::PreprocessedC),
"ir" => Ok(InputKind::Ir),
"h" => Ok(InputKind::CHeader),
"s" => Ok(InputKind::Assembler),
"S" | "sx" => Ok(InputKind::AssemblerWithCpp),
"cc" | "cpp" | "cxx" | "c++" | "C" | "hpp" | "hxx" | "ii" | "m" | "mm" => {
Err(XError::Unsupported(ext.to_owned()))
}
_ => Ok(InputKind::LinkerInput),
}
}
fn full_sequence(self) -> &'static [Phase] {
use Phase::{Assemble, Compile, Link, Preprocess};
match self {
InputKind::C | InputKind::CHeader => &[Preprocess, Compile, Assemble, Link],
InputKind::PreprocessedC | InputKind::Ir => &[Compile, Assemble, Link],
InputKind::AssemblerWithCpp => &[Preprocess, Assemble, Link],
InputKind::Assembler => &[Assemble, Link],
InputKind::LinkerInput => &[Link],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XError {
Unknown(String),
Unsupported(String),
}
impl std::fmt::Display for XError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
XError::Unknown(name) => {
write!(
f,
"unknown language `{name}`; \
accepted: c, c-header, cpp-output, ir, assembler, assembler-with-cpp, none"
)
}
XError::Unsupported(name) => {
write!(
f,
"`{name}` is not C, and this compiler is only ever going to compile C; \
see the not-in-scope list in spec/00-README.md"
)
}
}
}
}
impl std::error::Error for XError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
File,
Library,
Linker,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Input {
pub path: String,
pub forced: Option<InputKind>,
pub role: Role,
}
impl Input {
#[must_use]
pub fn new(path: impl Into<String>) -> Input {
Input { path: path.into(), forced: None, role: Role::File }
}
#[must_use]
pub fn library(name: impl Into<String>) -> Input {
Input { path: name.into(), forced: None, role: Role::Library }
}
#[must_use]
pub fn linker(arg: impl Into<String>) -> Input {
Input { path: arg.into(), forced: None, role: Role::Linker }
}
#[must_use]
pub fn named(&self) -> String {
match self.role {
Role::File => self.path.clone(),
Role::Library => format!("-l{}", self.path),
Role::Linker => format!("-Wl,{}", self.path),
}
}
pub fn kind(&self) -> Result<InputKind, XError> {
if self.role != Role::File {
return Ok(InputKind::LinkerInput);
}
match self.forced {
Some(k) => Ok(k),
None => InputKind::from_path(&self.path),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Output {
Stdout,
File(String),
Temporary(String),
}
impl Output {
fn render(&self) -> String {
match self {
Output::Stdout => "-".to_owned(),
Output::File(p) => p.clone(),
Output::Temporary(p) => format!("{p} (temporary)"),
}
}
fn as_link_input(&self) -> Option<&str> {
match self {
Output::File(p) | Output::Temporary(p) => Some(p),
Output::Stdout => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Job {
pub input: String,
pub kind: InputKind,
pub phases: Vec<Phase>,
pub output: Output,
pub aux_base: Option<String>,
}
impl Job {
#[must_use]
pub fn saved_text(&self) -> Option<String> {
let base = self.aux_base.as_ref()?;
self.phases.contains(&Phase::Preprocess).then(|| format!("{base}.i"))
}
#[must_use]
pub fn saved_asm(&self) -> Option<String> {
let base = self.aux_base.as_ref()?;
let past = self.phases.last().is_some_and(|last| *last > Phase::Compile);
(past && self.phases.contains(&Phase::Compile)).then(|| format!("{base}.s"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkJob {
pub inputs: Vec<Item>,
pub output: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchiveJob {
pub members: Vec<String>,
pub output: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Plan {
pub jobs: Vec<Job>,
pub link: Option<LinkJob>,
pub archive: Option<ArchiveJob>,
pub notes: Vec<String>,
pub output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanError {
pub message: String,
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for PlanError {}
fn plan_err(message: impl Into<String>) -> PlanError {
PlanError { message: message.into() }
}
#[must_use]
pub fn last_phase(emit: EmitKind) -> Phase {
match emit {
EmitKind::Preprocessed => Phase::Preprocess,
EmitKind::Asm
| EmitKind::Tast
| EmitKind::Ir
| EmitKind::MirFinal
| EmitKind::SafetySummary
| EmitKind::TypeGranules => Phase::Compile,
EmitKind::Object => Phase::Assemble,
EmitKind::Archive => Phase::Archive,
EmitKind::Executable => Phase::Link,
}
}
fn extension(path: &str) -> &str {
let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
match name.rfind('.') {
Some(0) | None => "",
Some(i) => &name[i + 1..],
}
}
fn file_part(path: &str) -> &str {
path.rsplit(['/', '\\']).next().unwrap_or(path)
}
fn without_extension(path: &str) -> &str {
let start = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
match path[start..].rfind('.') {
Some(0) | None => path,
Some(i) => &path[..start + i],
}
}
fn stem(path: &str) -> &str {
file_part(without_extension(path))
}
fn aux_base(opts: &Options, input: &str, output: Option<&str>, collecting: bool) -> String {
let named = match output {
Some(o) => without_extension(o),
None if collecting => stem(default_exe(opts)),
None => stem(input),
};
let named = if opts.save_temps == SaveTemps::Cwd { file_part(named) } else { named };
if collecting { format!("{named}-{}", stem(input)) } else { named.to_owned() }
}
fn suffix_for(phase: Phase, opts: &Options) -> &'static str {
match phase {
Phase::Preprocess => "i",
Phase::Compile => match opts.emit {
EmitKind::Tast => "tast",
EmitKind::Ir => "ir",
EmitKind::MirFinal => "mir",
EmitKind::SafetySummary => "safety.json",
EmitKind::TypeGranules => "granules.txt",
_ => "s",
},
Phase::Assemble => {
if opts.target.os == Os::Windows {
"obj"
} else {
"o"
}
}
Phase::Archive | Phase::Link => "",
}
}
fn default_exe(opts: &Options) -> &'static str {
if opts.target.os == Os::Windows { "a.exe" } else { "a.out" }
}
impl Plan {
pub fn new(opts: &Options, inputs: &[Input], output: Option<&str>) -> Result<Plan, PlanError> {
if inputs.is_empty() {
return Err(plan_err("no input files"));
}
let last = last_phase(opts.emit);
let linking = last == Phase::Link;
let archiving = last == Phase::Archive;
if archiving && output.is_none() {
return Err(plan_err("an archive has no default name, so `--emit=archive` needs `-o`"));
}
let collecting = linking || archiving;
let mut kinds = Vec::with_capacity(inputs.len());
for input in inputs {
kinds.push(input.kind().map_err(|e| plan_err(format!("{}: {e}", input.path)))?);
}
let producing = if collecting {
0
} else {
kinds
.iter()
.filter(|k| **k != InputKind::LinkerInput)
.filter(|k| k.full_sequence().iter().any(|p| *p <= last))
.count()
};
if output.is_some() && !collecting && producing > 1 {
return Err(plan_err("cannot specify -o with multiple inputs when not linking"));
}
let mut notes = Vec::new();
let mut jobs = Vec::with_capacity(inputs.len());
let mut link_inputs = Vec::new();
let mut members = Vec::new();
for (input, kind) in inputs.iter().zip(kinds) {
if input.role == Role::Linker {
if linking {
link_inputs.push(Item::Linker(input.path.clone()));
}
continue;
}
if kind == InputKind::LinkerInput {
if archiving {
let named = input.named();
return Err(plan_err(format!(
"{named}: an archive is written from the objects this command line \
compiles, and the symbol index in it needs the names each member \
defines, which this compiler knows for a file it compiled and not for \
one it was handed"
)));
}
if linking {
link_inputs.push(if input.role == Role::Library {
Item::Library(input.path.clone())
} else {
Item::File(input.path.clone())
});
} else {
notes.push(format!(
"{}: linker input unused because linking was not requested",
input.named()
));
}
if input.role == Role::Library {
continue;
}
jobs.push(Job {
input: input.path.clone(),
kind,
phases: Vec::new(),
output: Output::File(input.path.clone()),
aux_base: None,
});
continue;
}
let phases: Vec<Phase> =
kind.full_sequence().iter().copied().filter(|p| *p <= last).collect();
let Some(&final_phase) = phases.last() else {
notes.push(format!(
"{}: input unused because it enters the pipeline after the last phase \
the mode flags asked for",
input.path
));
jobs.push(Job {
input: input.path.clone(),
kind,
phases,
output: Output::File(input.path.clone()),
aux_base: None,
});
continue;
};
let named = if producing == 1 { output } else { None };
let aux = (opts.save_temps.wanted() && final_phase > Phase::Preprocess)
.then(|| aux_base(opts, &input.path, output, collecting));
let out = if final_phase == Phase::Link || archiving {
let ext = suffix_for(Phase::Assemble, opts);
match &aux {
Some(base) => Output::File(format!("{base}.{ext}")),
None => Output::Temporary(format!("{}.{ext}", stem(&input.path))),
}
} else if let Some(o) = named {
if o == "-" { Output::Stdout } else { Output::File(o.to_owned()) }
} else if final_phase == Phase::Preprocess {
Output::Stdout
} else {
Output::File(format!("{}.{}", stem(&input.path), suffix_for(final_phase, opts)))
};
if let Output::File(path) = &out {
if *path == input.path {
return Err(plan_err(format!(
"input file `{}` is the same as the output file",
input.path
)));
}
}
if linking {
if let Some(p) = out.as_link_input() {
link_inputs.push(Item::File(p.to_owned()));
}
}
if archiving {
members.push(format!(
"{}.{}",
stem(&input.path),
suffix_for(Phase::Assemble, opts)
));
}
jobs.push(Job { input: input.path.clone(), kind, phases, output: out, aux_base: aux });
}
let link = linking.then(|| LinkJob {
inputs: link_inputs,
output: output.unwrap_or(default_exe(opts)).to_owned(),
});
let archive = archiving.then(|| ArchiveJob {
members,
output: output.unwrap_or_default().to_owned(),
});
Ok(Plan { jobs, link, archive, notes, output: output.map(str::to_owned) })
}
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
for note in &self.notes {
let _ = writeln!(out, "note: {note}");
}
for job in &self.jobs {
if job.phases.is_empty() {
continue;
}
let names: Vec<&str> = job.phases.iter().map(|p| p.as_str()).collect();
let _ = writeln!(out, "{}: {} -> {}", job.input, names.join(", "), job.output.render());
let kept: Vec<String> =
[job.saved_text(), job.saved_asm()].into_iter().flatten().collect();
if !kept.is_empty() {
let _ = writeln!(out, "{}: keeping {}", job.input, kept.join(", "));
}
}
if let Some(link) = &self.link {
let names: Vec<String> = link.inputs.iter().map(ToString::to_string).collect();
let _ = writeln!(out, "link: {} -> {}", names.join(" "), link.output);
}
if let Some(archive) = &self.archive {
let _ = writeln!(out, "archive: {} -> {}", archive.members.join(" "), archive.output);
}
out
}
}
#[cfg(test)]
mod tests {
use rucc_session::Options;
use super::*;
fn opts(triple: &str) -> Options {
Options::new(triple.parse().expect("test triple"))
}
fn linux() -> Options {
opts("x86_64-unknown-linux-gnu")
}
fn plan(o: &Options, paths: &[&str], output: Option<&str>) -> Plan {
let inputs: Vec<Input> = paths.iter().map(|p| Input::new(*p)).collect();
Plan::new(o, &inputs, output).expect("expected a plan")
}
#[test]
fn extensions_map_to_the_table_in_the_spec() {
assert_eq!(InputKind::from_path("a.c").unwrap(), InputKind::C);
assert_eq!(InputKind::from_path("a.i").unwrap(), InputKind::PreprocessedC);
assert_eq!(InputKind::from_path("a.ir").unwrap(), InputKind::Ir);
assert_eq!(InputKind::from_path("a.h").unwrap(), InputKind::CHeader);
assert_eq!(InputKind::from_path("a.s").unwrap(), InputKind::Assembler);
assert_eq!(InputKind::from_path("a.S").unwrap(), InputKind::AssemblerWithCpp);
assert_eq!(InputKind::from_path("a.sx").unwrap(), InputKind::AssemblerWithCpp);
assert_eq!(InputKind::from_path("a.o").unwrap(), InputKind::LinkerInput);
assert_eq!(InputKind::from_path("libm.a").unwrap(), InputKind::LinkerInput);
assert_eq!(InputKind::from_path("libm.so.6").unwrap(), InputKind::LinkerInput);
}
#[test]
fn ir_enters_where_preprocessed_c_does_and_needs_no_preprocessor() {
assert_eq!(InputKind::from_x_arg("ir").unwrap(), InputKind::Ir);
assert_eq!(InputKind::Ir.as_str(), "ir");
assert_eq!(InputKind::Ir.full_sequence(), InputKind::PreprocessedC.full_sequence());
assert!(!InputKind::Ir.full_sequence().contains(&Phase::Preprocess));
}
#[test]
fn an_input_whose_output_has_its_own_name_is_refused_rather_than_written_over() {
let mut o = linux();
o.emit = EmitKind::Ir;
let inputs = [Input::new("a.ir")];
let error = Plan::new(&o, &inputs, None).expect_err("expected this to be refused");
assert!(error.message.contains("is the same as the output file"), "{error}");
assert!(Plan::new(&o, &inputs, Some("b.ir")).is_ok());
assert!(Plan::new(&o, &inputs, Some("a.ir")).is_err());
}
#[test]
fn capital_s_and_small_s_are_different_languages() {
let hi = InputKind::from_path("a.S").unwrap();
let lo = InputKind::from_path("a.s").unwrap();
assert_ne!(hi, lo);
assert!(hi.full_sequence().contains(&Phase::Preprocess));
assert!(!lo.full_sequence().contains(&Phase::Preprocess));
}
#[test]
fn a_cplusplus_source_says_why_rather_than_failing_at_link_time() {
let e = InputKind::from_path("a.cpp").unwrap_err();
assert!(format!("{e}").contains("only ever going to compile C"), "{e}");
let e = InputKind::from_x_arg("c++").unwrap_err();
assert!(matches!(e, XError::Unsupported(_)), "{e:?}");
}
#[test]
fn a_file_with_no_extension_goes_to_the_linker() {
assert_eq!(InputKind::from_path("crt1").unwrap(), InputKind::LinkerInput);
assert_eq!(InputKind::from_path(".bashrc").unwrap(), InputKind::LinkerInput);
}
#[test]
fn the_default_line_compiles_and_links_to_a_out() {
let p = plan(&linux(), &["a.c"], None);
assert_eq!(
p.jobs[0].phases,
vec![Phase::Preprocess, Phase::Compile, Phase::Assemble, Phase::Link]
);
assert_eq!(p.jobs[0].output, Output::Temporary("a.o".into()));
let link = p.link.expect("expected a link step");
assert_eq!(link.inputs, vec![Item::File("a.o".into())]);
assert_eq!(link.output, "a.out");
}
#[test]
fn dash_c_stops_at_the_object_and_names_it_after_the_source() {
let mut o = linux();
o.emit = EmitKind::Object;
let p = plan(&o, &["src/a.c", "src/b.c"], None);
assert!(p.link.is_none());
assert_eq!(p.jobs[0].output, Output::File("a.o".into()));
assert_eq!(p.jobs[1].output, Output::File("b.o".into()));
assert_eq!(p.jobs[0].phases.last(), Some(&Phase::Assemble));
}
#[test]
fn dash_e_writes_to_stdout_unless_it_is_given_a_name() {
let mut o = linux();
o.emit = EmitKind::Preprocessed;
assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::Stdout);
assert_eq!(plan(&o, &["a.c"], Some("a.i")).jobs[0].output, Output::File("a.i".into()));
}
#[test]
fn a_name_of_one_dash_is_standard_output_and_not_a_file_called_that() {
let mut o = linux();
o.emit = EmitKind::Preprocessed;
assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
o.emit = EmitKind::Object;
assert_eq!(plan(&o, &["a.c"], Some("-")).jobs[0].output, Output::Stdout);
let p = plan(&linux(), &["a.c"], Some("-"));
assert_eq!(p.link.expect("a link step").output, "-");
}
#[test]
fn dash_s_produces_assembly_named_after_the_source() {
let mut o = linux();
o.emit = EmitKind::Asm;
let p = plan(&o, &["dir/a.c"], None);
assert_eq!(p.jobs[0].output, Output::File("a.s".into()));
assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Compile]);
}
#[test]
fn an_already_preprocessed_file_skips_the_preprocessor() {
let p = plan(&linux(), &["a.i"], None);
assert_eq!(p.jobs[0].phases, vec![Phase::Compile, Phase::Assemble, Phase::Link]);
}
#[test]
fn assembly_with_a_capital_s_is_preprocessed_but_not_compiled() {
let p = plan(&linux(), &["a.S"], None);
assert_eq!(p.jobs[0].phases, vec![Phase::Preprocess, Phase::Assemble, Phase::Link]);
assert!(!p.jobs[0].phases.contains(&Phase::Compile));
}
#[test]
fn objects_on_the_line_reach_the_linker_in_the_order_they_were_written() {
let p = plan(&linux(), &["a.o", "b.c", "libm.a"], None);
let link = p.link.expect("expected a link step");
assert_eq!(
link.inputs,
vec![Item::File("a.o".into()), Item::File("b.o".into()), Item::File("libm.a".into()),]
);
}
#[test]
fn an_object_on_a_dash_c_line_is_a_note_rather_than_an_error() {
let mut o = linux();
o.emit = EmitKind::Object;
let p = plan(&o, &["a.c", "b.o"], None);
assert!(p.jobs[1].phases.is_empty());
assert_eq!(p.notes.len(), 1);
assert!(p.notes[0].contains("linker input unused"), "{:?}", p.notes);
}
#[test]
fn dash_o_with_several_compilations_is_rejected() {
let mut o = linux();
o.emit = EmitKind::Object;
let inputs = [Input::new("a.c"), Input::new("b.c")];
let e = Plan::new(&o, &inputs, Some("out.o")).unwrap_err();
assert!(e.message.contains("multiple inputs"), "{}", e.message);
}
#[test]
fn dash_o_with_one_compilation_and_some_objects_is_fine() {
let mut o = linux();
o.emit = EmitKind::Object;
let inputs = [Input::new("a.c"), Input::new("b.o")];
let p = Plan::new(&o, &inputs, Some("out.o")).expect("expected a plan");
assert_eq!(p.jobs[0].output, Output::File("out.o".into()));
}
#[test]
fn dash_x_overrides_the_extension() {
let inputs = [Input { path: "a.txt".into(), forced: Some(InputKind::C), role: Role::File }];
let p = Plan::new(&linux(), &inputs, None).expect("expected a plan");
assert_eq!(p.jobs[0].kind, InputKind::C);
assert_eq!(p.jobs[0].phases.first(), Some(&Phase::Preprocess));
}
#[test]
fn windows_gets_obj_and_a_exe() {
let o = opts("x86_64-pc-windows-msvc");
let p = plan(&o, &["a.c"], None);
assert_eq!(p.jobs[0].output, Output::Temporary("a.obj".into()));
assert_eq!(p.link.expect("expected a link step").output, "a.exe");
}
#[test]
fn an_archive_is_one_file_however_many_inputs_there_are() {
let mut o = linux();
o.emit = EmitKind::Archive;
let p = plan(&o, &["a.c", "sub/b.c"], Some("out/libx.a"));
assert_eq!(p.jobs.len(), 2);
for job in &p.jobs {
assert_eq!(job.phases.last(), Some(&Phase::Assemble), "{}", job.input);
assert!(matches!(job.output, Output::Temporary(_)), "{:?}", job.output);
}
let archive = p.archive.expect("an archive step");
assert_eq!(archive.members, ["a.o", "b.o"]);
assert_eq!(archive.output, "out/libx.a");
assert!(p.link.is_none(), "one command line produces one of the two and not both");
}
#[test]
fn a_member_is_called_what_an_object_is_called_on_this_target() {
let mut o = opts("x86_64-pc-windows-msvc");
o.emit = EmitKind::Archive;
let p = plan(&o, &["a.c"], Some("x.lib"));
assert_eq!(p.archive.expect("an archive step").members, ["a.obj"]);
}
#[test]
fn an_archive_has_no_default_name() {
let mut o = linux();
o.emit = EmitKind::Archive;
let inputs = [Input::new("a.c")];
let error = Plan::new(&o, &inputs, None).expect_err("no name for the archive");
assert!(error.message.contains("needs `-o`"), "{error}");
}
#[test]
fn something_this_compilation_did_not_produce_cannot_go_into_an_archive() {
let mut o = linux();
o.emit = EmitKind::Archive;
for handed in [Input::new("b.o"), Input::library("m")] {
let inputs = [Input::new("a.c"), handed];
let error = Plan::new(&o, &inputs, Some("libx.a")).expect_err("not ours to index");
assert!(error.message.contains("names each member"), "{error}");
}
}
#[test]
fn the_plan_says_what_goes_into_the_archive() {
let mut o = linux();
o.emit = EmitKind::Archive;
let text = plan(&o, &["a.c", "b.c"], Some("libx.a")).render();
assert!(text.contains("archive: a.o b.o -> libx.a"), "{text}");
assert!(!text.contains("link:"), "{text}");
}
#[test]
fn the_intermediate_dumps_stop_where_dash_s_stops() {
for emit in [
EmitKind::Tast,
EmitKind::Ir,
EmitKind::MirFinal,
EmitKind::SafetySummary,
EmitKind::TypeGranules,
] {
assert_eq!(last_phase(emit), Phase::Compile, "{emit:?}");
}
}
#[test]
fn each_intermediate_dump_is_a_language_of_its_own_and_gets_a_suffix_of_its_own() {
for (emit, name) in [
(EmitKind::Asm, "a.s"),
(EmitKind::Tast, "a.tast"),
(EmitKind::Ir, "a.ir"),
(EmitKind::MirFinal, "a.mir"),
(EmitKind::SafetySummary, "a.safety.json"),
(EmitKind::TypeGranules, "a.granules.txt"),
] {
let mut o = linux();
o.emit = emit;
assert_eq!(plan(&o, &["a.c"], None).jobs[0].output, Output::File(name.into()));
}
}
#[test]
fn an_input_that_enters_after_the_last_phase_is_a_note_rather_than_an_error() {
let mut o = linux();
o.emit = EmitKind::Preprocessed;
let p = plan(&o, &["a.c", "b.s"], None);
assert!(p.jobs[1].phases.is_empty());
assert_eq!(p.notes.len(), 1);
assert!(p.notes[0].contains("after the last phase"), "{:?}", p.notes);
let inputs = [Input::new("a.c"), Input::new("b.s")];
assert!(Plan::new(&o, &inputs, Some("out.i")).is_ok());
}
#[test]
fn no_inputs_is_an_error() {
assert!(Plan::new(&linux(), &[], None).is_err());
}
fn keeping(kind: SaveTemps, emit: EmitKind, paths: &[&str], output: Option<&str>) -> Plan {
let mut o = linux();
o.emit = emit;
o.save_temps = kind;
plan(&o, paths, output)
}
fn kept(plan: &Plan, at: usize) -> Vec<String> {
[plan.jobs[at].saved_text(), plan.jobs[at].saved_asm()].into_iter().flatten().collect()
}
#[test]
fn the_files_that_are_kept_land_beside_the_output_and_not_where_the_manual_says() {
let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/t.o"));
assert_eq!(kept(&p, 0), vec!["out/t.i", "out/t.s"]);
let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/t.o"));
assert_eq!(kept(&p, 0), vec!["t.i", "t.s"]);
}
#[test]
fn the_name_comes_off_the_output_rather_than_off_the_input_that_produced_it() {
let p = keeping(SaveTemps::Cwd, EmitKind::Object, &["t.c"], Some("out/x.o"));
assert_eq!(kept(&p, 0), vec!["x.i", "x.s"]);
let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.c"], Some("out/noext"));
assert_eq!(kept(&p, 0), vec!["out/noext.i", "out/noext.s"]);
}
#[test]
fn without_a_name_they_are_called_after_the_input_and_are_where_the_object_would_be() {
for kind in [SaveTemps::Object, SaveTemps::Cwd] {
let p = keeping(kind, EmitKind::Object, &["sub/u.c"], None);
assert_eq!(kept(&p, 0), vec!["u.i", "u.s"], "{kind:?}");
assert_eq!(p.jobs[0].output, Output::File("u.o".into()), "{kind:?}");
}
}
#[test]
fn a_command_line_that_links_names_them_after_the_executable_and_the_input() {
let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c", "sub/u.c"], Some("o/p"));
assert_eq!(kept(&p, 0), vec!["o/p-t.i", "o/p-t.s"]);
assert_eq!(kept(&p, 1), vec!["o/p-u.i", "o/p-u.s"]);
let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], None);
assert_eq!(kept(&p, 0), vec!["a-t.i", "a-t.s"]);
}
#[test]
fn the_object_a_link_reads_is_kept_rather_than_written_where_it_will_be_removed() {
let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.c"], Some("out/prog"));
assert_eq!(p.jobs[0].output, Output::File("out/prog-t.o".into()));
let plain = plan(&linux(), &["t.c"], Some("out/prog"));
assert_eq!(plain.jobs[0].output, Output::Temporary("t.o".into()));
}
#[test]
fn a_step_whose_result_is_already_being_written_is_not_kept_a_second_time() {
let p = keeping(SaveTemps::Object, EmitKind::Preprocessed, &["t.c"], None);
assert_eq!(p.jobs[0].aux_base, None);
assert_eq!(kept(&p, 0), Vec::<String>::new());
let p = keeping(SaveTemps::Object, EmitKind::Asm, &["t.c"], None);
assert_eq!(kept(&p, 0), vec!["t.i"]);
}
#[test]
fn an_input_that_arrives_preprocessed_has_no_text_of_its_own_to_keep() {
let p = keeping(SaveTemps::Object, EmitKind::Object, &["t.i"], None);
assert_eq!(kept(&p, 0), vec!["t.s"]);
let p = keeping(SaveTemps::Object, EmitKind::Executable, &["t.o"], None);
assert_eq!(p.jobs[0].aux_base, None);
}
#[test]
fn nothing_is_kept_when_the_flag_was_not_given() {
let p = plan(&linux(), &["t.c"], None);
assert_eq!(p.jobs[0].aux_base, None);
assert_eq!(kept(&p, 0), Vec::<String>::new());
}
#[test]
fn the_rendering_says_what_will_happen() {
let p = plan(&linux(), &["a.c", "b.o"], None);
let text = p.render();
assert!(
text.contains("a.c: preprocess, compile, assemble, link -> a.o (temporary)"),
"{text}"
);
assert!(text.contains("link: a.o b.o -> a.out"), "{text}");
assert_eq!(text.matches("b.o").count(), 1, "{text}");
}
#[test]
fn the_rendering_names_the_files_that_will_be_kept() {
let p = keeping(SaveTemps::Object, EmitKind::Executable, &["a.c"], None);
let text = p.render();
assert!(text.contains("a.c: keeping a-a.i, a-a.s"), "{text}");
assert!(!plan(&linux(), &["a.c"], None).render().contains("keeping"));
}
}