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",
),
];
pub static NAMES: &[(&str, &str, &str)] = &[
("mul.i8", "a narrow multiply, which the integer promotions mean C never asks for", NARROW),
("mul.i16", "the same", NARROW),
("sdiv.i8", "the same, and a divide besides, which is never narrow either", NARROW),
("sdiv.i16", "the same", NARROW),
("udiv.i8", "the same", NARROW),
("udiv.i16", "the same", NARROW),
("srem.i8", "the same", NARROW),
("srem.i16", "the same", NARROW),
("urem.i8", "the same", NARROW),
("urem.i16", "the same", NARROW),
("xor.i8", "the same, and no bitfield is written with one", NARROW),
("xor.i16", "the same", NARROW),
("zext.i1.i8", "a truth value widened to a byte, which nothing asks for at that width", NARROW),
("zext.i1.i16", "the same", NARROW),
];
const NARROW: &str = "tamnd/rucc#375";
#[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 deferred: 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, {} have no lowering yet and {} names are left for later",
self.source,
self.by_rule.len(),
self.opcodes,
self.names,
self.elsewhere.len(),
self.gaps.len(),
self.deferred.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();
let mut deferred = Vec::new();
for &(opcode, name) in &named {
if patterns.contains(&name) {
by_rule.push(opcode);
} else if NAMES.iter().any(|&(deliberate, ..)| deliberate == name) {
deferred.push((opcode, name));
} 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() - deferred.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,
deferred,
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 a_name_a_rule_is_written_at_is_not_a_name_left_for_later() {
let heads = pattern_heads(&TABLE);
let named = term::heads();
for &(name, why, issue) in NAMES {
assert!(
!heads.contains(&name),
"`{name}` is lowered by a rule now, so the `NAMES` entry saying it is {why} is \
stale and {issue} may be closer than it says"
);
assert!(
named.iter().any(|&(_, head)| head == name),
"`{name}` is not a name any instruction can have, so the `NAMES` entry excuses \
nothing"
);
}
let report = report(&TABLE);
assert_eq!(report.deferred.len(), NAMES.len(), "{:?}", report.deferred);
}
#[test]
fn every_gap_names_the_issue_that_closes_it() {
let issues = GAPS
.iter()
.map(|&(_, _, issue)| issue)
.chain(WIDTHS.iter().map(|&(_, _, issue)| issue))
.chain(NAMES.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}");
}
for &(name, why, issue) in NAMES {
println!("rucc-codegen: no rule at `{name}`, 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);
}
}