use core::fmt;
use core::fmt::Write as _;
use rucc_ir::Opcode;
use rucc_target::Arch;
use crate::select::{Table, Test};
use crate::term;
pub static ELSEWHERE: &[(Opcode, &str)] = &[
(Opcode::Call, "`crate::abi`, which builds a call out of the convention"),
(Opcode::CallIndirect, "`crate::abi`, the same instruction with the callee in a register"),
(Opcode::Alloca, "`crate::lower`, as an address into a frame `crate::frame` lays out later"),
(Opcode::GlobalAddr, "`crate::lower`, a `lea` off the instruction pointer with a name on it"),
(Opcode::PtrToInt, "`crate::lower`, which renames the value rather than computing anything"),
(Opcode::IntToPtr, "`crate::lower`, the same rename the other way round"),
(Opcode::Jump, "`crate::layout`, since an edge is on the block and not in the block"),
(Opcode::Unreachable, "nothing at all, which is the answer for a place control does not reach"),
(Opcode::UnreachableHint, "nothing at all, for the same reason"),
(Opcode::Switch, "`crate::expand`, into the compare and branch chain it is"),
(Opcode::FConst, "`crate::expand`, into a constant in memory and a load of it"),
(Opcode::FNeg, "`crate::expand`, into the sign bit flip it is"),
(Opcode::UIToFP, "`crate::expand`, into a widening and a signed conversion"),
(Opcode::FPToUI, "`crate::expand`, into a signed conversion and a narrowing"),
(Opcode::Memcpy, "`crate::expand`, into the moves it stands for"),
(Opcode::Memset, "`crate::expand`, into the fills it stands for"),
(Opcode::Memmove, "`crate::expand`, into a call, since the two regions may overlap"),
(Opcode::VaStart, "`crate::varargs`, which writes the register save area the ABI describes"),
(Opcode::VaArg, "`crate::varargs`, into the walk over that structure"),
(Opcode::VaObject, "`crate::varargs`, the same walk for something that arrived in memory"),
(Opcode::VaCopy, "`crate::varargs`, into a copy of the structure"),
(Opcode::VaEnd, "`crate::varargs`, which removes it, since there is nothing to undo"),
];
pub static GAPS: &[(Opcode, &str, &str)] = &[
(Opcode::Splat, "a vector, and no rule is written about a lane count", "tamnd/rucc#200"),
(
Opcode::TargetIntrinsic,
"the same, since what needs one is a vector builtin",
"tamnd/rucc#200",
),
(Opcode::BlockAddr, "the address of a label", "tamnd/rucc#353"),
(Opcode::IndirectBr, "the branch a computed goto turns into", "tamnd/rucc#353"),
(
Opcode::FRem,
"a call to `fmod`, so a link line question as much as a lowering one",
"tamnd/rucc#226",
),
(
Opcode::Fma,
"a call or one instruction, depending on what the machine is told it has",
"tamnd/rucc#226",
),
(Opcode::AtomicLoad, "an ordering, which the IR cannot say yet", "tamnd/rucc#311"),
(Opcode::AtomicStore, "the same", "tamnd/rucc#311"),
(Opcode::AtomicRmw, "the same, and a `lock` prefix per operation", "tamnd/rucc#311"),
(Opcode::Cmpxchg, "the same, and a result that is a pair", "tamnd/rucc#311"),
(
Opcode::Fence,
"the same, and nothing at all on this machine for most orderings",
"tamnd/rucc#311",
),
(
Opcode::Ctlz,
"one instruction on a machine that has it and several on one that does not",
"tamnd/rucc#310",
),
(Opcode::Cttz, "the same", "tamnd/rucc#310"),
(Opcode::Ctpop, "the same", "tamnd/rucc#310"),
(Opcode::Bswap, "three instructions and no rule", "tamnd/rucc#307"),
(Opcode::Bitreverse, "a node nothing writes and nothing lowers", "tamnd/rucc#363"),
(
Opcode::SAddOverflow,
"a result and a flag together, which no rule can write",
"tamnd/rucc#309",
),
(Opcode::UAddOverflow, "the same", "tamnd/rucc#309"),
(Opcode::SSubOverflow, "the same", "tamnd/rucc#309"),
(Opcode::USubOverflow, "the same", "tamnd/rucc#309"),
(Opcode::SMulOverflow, "the same", "tamnd/rucc#309"),
(Opcode::UMulOverflow, "the same", "tamnd/rucc#309"),
(Opcode::Expect, "a branch weight nothing reads yet", "tamnd/rucc#364"),
(Opcode::Prefetch, "one instruction, once the hints have somewhere to go", "tamnd/rucc#313"),
(Opcode::FrameAddress, "a walk up the frame pointers", "tamnd/rucc#312"),
(Opcode::ReturnAddress, "the same walk, one word further along", "tamnd/rucc#312"),
(
Opcode::StackSave,
"a frame that can grow, as a variable length array needs",
"tamnd/rucc#291",
),
(Opcode::StackRestore, "the same", "tamnd/rucc#291"),
(
Opcode::SetjmpMarker,
"a call that returns twice, which the allocator has to be told about",
"tamnd/rucc#223",
),
(Opcode::LongjmpMarker, "the same", "tamnd/rucc#223"),
(Opcode::TailCall, "a terminator nothing writes and nothing lowers", "tamnd/rucc#365"),
(
Opcode::InlineAsm,
"a template, its constraints, and sixty eight torture programs",
"tamnd/rucc#349",
),
];
pub static WIDTHS: &[(&str, &str, &str)] = &[
(
"one bit",
"everything but and, or, xor, a constant, and the widening out of one",
"tamnd/rucc#352",
),
(
"a hundred and twenty eight bits",
"no register pair, so nothing at that width has a name",
"tamnd/rucc#351",
),
(
"eighty bits",
"a long double is on the x87 stack and no rule is about that stack",
"tamnd/rucc#326",
),
(
"a vector of any lane count",
"a rule at a width says nothing about how many lanes",
"tamnd/rucc#200",
),
];
#[derive(Debug)]
pub struct Report {
pub source: &'static str,
pub opcodes: usize,
pub by_rule: Vec<Opcode>,
pub names: usize,
pub uncovered: Vec<(Opcode, &'static str)>,
pub unreachable: Vec<&'static str>,
pub elsewhere: Vec<Opcode>,
pub gaps: Vec<Opcode>,
pub unaccounted: Vec<Opcode>,
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"rucc-codegen: {} lowers {} of the {} IR opcodes by rule at {} names, {} are lowered \
where no rule reaches and {} have no lowering yet",
self.source,
self.by_rule.len(),
self.opcodes,
self.names,
self.elsewhere.len(),
self.gaps.len()
)
}
}
#[must_use]
pub fn report(table: &Table) -> Report {
let named = term::heads();
let patterns = pattern_heads(table);
let mut by_rule = Vec::new();
let mut uncovered = Vec::new();
for &(opcode, name) in &named {
if patterns.contains(&name) {
by_rule.push(opcode);
} else {
uncovered.push((opcode, name));
}
}
for &(opcode, _) in &uncovered {
by_rule.retain(|&covered| covered != opcode);
}
by_rule.sort_unstable();
by_rule.dedup();
let names = named.len() - uncovered.len();
let unreachable: Vec<&'static str> = patterns
.iter()
.filter(|head| !named.iter().any(|(_, name)| name == *head))
.copied()
.collect();
let elsewhere: Vec<Opcode> = ELSEWHERE.iter().map(|&(opcode, _)| opcode).collect();
let gaps: Vec<Opcode> = GAPS.iter().map(|&(opcode, ..)| opcode).collect();
let unaccounted: Vec<Opcode> = Opcode::all()
.filter(|opcode| {
!by_rule.contains(opcode) && !elsewhere.contains(opcode) && !gaps.contains(opcode)
})
.collect();
Report {
source: table.source,
opcodes: Opcode::all().count(),
by_rule,
names,
uncovered,
unreachable,
elsewhere,
gaps,
unaccounted,
}
}
fn pattern_heads(table: &Table) -> Vec<&'static str> {
let Some(root) = table.nodes.first() else { return Vec::new() };
let mut found: Vec<&'static str> = root
.tests
.iter()
.filter_map(|(test, _)| match test {
Test::App { head, .. } => Some(*head),
Test::Int(_) => None,
})
.collect();
found.sort_unstable();
found.dedup();
found
}
#[must_use]
pub fn table(arch: Arch) -> Option<&'static Table> {
match arch {
Arch::X86_64 => Some(&crate::select::x86_64::TABLE),
Arch::Aarch64 | Arch::Riscv64 => None,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Fired {
seen: Vec<bool>,
}
impl Fired {
#[must_use]
pub const fn new() -> Fired {
Fired { seen: Vec::new() }
}
pub fn mark(&mut self, rule: usize) {
if self.seen.len() <= rule {
self.seen.resize(rule + 1, false);
}
self.seen[rule] = true;
}
#[must_use]
pub fn has(&self, rule: usize) -> bool {
self.seen.get(rule).copied().unwrap_or(false)
}
#[must_use]
pub fn count(&self) -> usize {
self.seen.iter().filter(|fired| **fired).count()
}
pub fn merge(&mut self, other: &Fired) {
if self.seen.len() < other.seen.len() {
self.seen.resize(other.seen.len(), false);
}
for (mine, theirs) in self.seen.iter_mut().zip(&other.seen) {
*mine |= *theirs;
}
}
#[must_use]
pub fn listing(&self, table: &Table) -> String {
let fired = table.rules.iter().enumerate().filter(|(index, _)| self.has(*index)).count();
let mut out = format!(
"# rucc rule coverage: {fired} of {} rules in {} fired\n",
table.rules.len(),
table.source
);
for (index, rule) in table.rules.iter().enumerate() {
let word = if self.has(index) { "fired" } else { "unused" };
let _ = writeln!(out, "{word} {}:{} {}", table.source, rule.line, rule.pattern);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::select::x86_64::TABLE;
#[test]
fn every_name_an_instruction_can_have_is_one_a_rule_is_written_at() {
let report = report(&TABLE);
assert!(
report.uncovered.is_empty(),
"nothing in {} lowers these, and each is an opcode at a width the rule language can \
spell: {:?}",
report.source,
report.uncovered
);
}
#[test]
fn every_name_a_rule_is_written_at_is_one_an_instruction_can_have() {
let report = report(&TABLE);
assert!(
report.unreachable.is_empty(),
"{} has rules for these and no instruction is ever called one: {:?}",
report.source,
report.unreachable
);
}
#[test]
fn every_opcode_is_lowered_or_is_a_gap_somebody_wrote_down() {
let report = report(&TABLE);
assert!(
report.unaccounted.is_empty(),
"no rule lowers these, `ELSEWHERE` does not say where they are lowered and `GAPS` \
does not say why they are not: {:?}",
report.unaccounted
);
assert_eq!(
report.by_rule.len() + report.elsewhere.len() + report.gaps.len(),
report.opcodes,
"the three lists overlap, so an opcode is counted twice"
);
}
#[test]
fn an_entry_a_rule_now_covers_is_a_stale_entry() {
let report = report(&TABLE);
for &(opcode, where_) in ELSEWHERE {
assert!(
!report.by_rule.contains(&opcode),
"`{}` is lowered by a rule now, so the `ELSEWHERE` entry saying it is lowered by \
{where_} is stale",
opcode.name()
);
}
for &(opcode, why, issue) in GAPS {
assert!(
!report.by_rule.contains(&opcode),
"`{}` is lowered by a rule now, so the `GAPS` entry saying it is {why} is stale \
and {issue} may be closed",
opcode.name()
);
assert!(
!report.elsewhere.contains(&opcode),
"`{}` is on both lists, so it is both lowered and not lowered",
opcode.name()
);
}
}
#[test]
fn every_gap_names_the_issue_that_closes_it() {
let issues = GAPS
.iter()
.map(|&(_, _, issue)| issue)
.chain(WIDTHS.iter().map(|&(_, _, issue)| issue));
for issue in issues {
let number = issue
.strip_prefix("tamnd/rucc#")
.unwrap_or_else(|| panic!("{issue} is not an issue in this project's tracker"));
assert!(number.parse::<u32>().is_ok(), "{issue} does not name an issue number");
}
}
#[test]
fn the_count_is_reported() {
let report = report(&TABLE);
println!("{report}");
for &(opcode, why, issue) in GAPS {
println!("rucc-codegen: no lowering for `{}`, which is {why}: {issue}", opcode.name());
}
for &(width, why, issue) in WIDTHS {
println!("rucc-codegen: no rule at {width}, which is {why}: {issue}");
}
assert_eq!(report.gaps.len(), GAPS.len());
}
#[test]
fn the_root_of_the_trie_is_the_head_of_every_pattern() {
let heads = pattern_heads(&TABLE);
assert!(!heads.is_empty(), "the table has rules and the root of the trie tests nothing");
for rule in TABLE.rules {
let head = rule
.pattern
.strip_prefix('(')
.and_then(|rest| rest.split([' ', ')']).next())
.expect("a pattern is an application");
assert!(
heads.contains(&head),
"line {}: {} is a pattern whose head the root of the trie does not test",
rule.line,
rule.pattern
);
}
}
#[test]
fn a_target_with_a_back_end_is_a_target_with_a_rule_set() {
let x86 = table(Arch::X86_64).expect("x86-64 is what this crate lowers for");
assert_eq!(x86.source, TABLE.source);
assert!(!x86.rules.is_empty());
assert!(table(Arch::Aarch64).is_none(), "there is no aarch64 rule file yet");
assert!(table(Arch::Riscv64).is_none(), "there is no riscv64 rule file yet");
}
#[test]
fn a_rule_is_written_down_as_the_place_it_is_written_at() {
let mut fired = Fired::new();
fired.mark(0);
let listing = fired.listing(&TABLE);
let first =
format!("fired {}:{} {}", TABLE.source, TABLE.rules[0].line, TABLE.rules[0].pattern);
assert!(listing.contains(&first), "{listing}");
assert!(listing.lines().next().is_some_and(|line| line.starts_with('#')), "{listing}");
}
#[test]
fn one_file_says_what_the_whole_rule_set_is() {
let listing = Fired::new().listing(&TABLE);
let lines: Vec<&str> = listing.lines().collect();
assert_eq!(lines.len(), TABLE.rules.len() + 1, "one line per rule and one for the count");
assert_eq!(
lines.iter().filter(|line| line.starts_with("unused ")).count(),
TABLE.rules.len()
);
assert!(lines[0].contains(&format!("0 of {} rules", TABLE.rules.len())), "{}", lines[0]);
}
#[test]
fn what_two_runs_reached_is_what_either_of_them_reached() {
let mut one = Fired::new();
one.mark(3);
one.mark(3);
assert_eq!(one.count(), 1, "a rule that fires twice is one rule");
let mut two = Fired::new();
two.mark(0);
two.mark(9);
one.merge(&two);
assert_eq!(one.count(), 3);
assert!(one.has(0) && one.has(3) && one.has(9));
assert!(!one.has(1));
let mut back = Fired::new();
back.mark(0);
back.mark(9);
let mut three = Fired::new();
three.mark(3);
back.merge(&three);
assert_eq!(back, one);
}
}