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::MemEntry, "nothing at all, since memory SSA comes off before the back end runs"),
(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 signed conversion with a widening or a halving"),
(Opcode::FPToUI, "`crate::expand`, into a signed conversion with a narrowing or a correction"),
(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::Bswap, "`crate::expand`, into the shifts and masks that reverse the bytes"),
(Opcode::AtomicLoad, "`crate::expand`, into the plain load that is already an acquire"),
(Opcode::AtomicStore, "`crate::expand`, into the plain store, and a barrier at the strongest"),
(
Opcode::Fence,
"`crate::lower`, as an `mfence` at the strongest ordering and nothing below it",
),
(Opcode::Ctpop, "`crate::expand`, into the halving sum that counts the set bits"),
(Opcode::Ctlz, "`crate::expand`, into a smear and a set bit count"),
(Opcode::Cttz, "`crate::expand`, into a mask of the low zeroes and a set bit count"),
(Opcode::UAddOverflow, "`crate::expand`, into an add and a comparison against an operand"),
(Opcode::SAddOverflow, "`crate::expand`, into an add and the sign bit of the operands"),
(Opcode::USubOverflow, "`crate::expand`, into a subtract and a comparison of the operands"),
(Opcode::SSubOverflow, "`crate::expand`, into a subtract and the sign bit of the operands"),
(Opcode::UMulOverflow, "`crate::expand`, into a multiply and the high half of the product"),
(Opcode::SMulOverflow, "`crate::expand`, into the same, with the high half corrected for sign"),
(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"),
(Opcode::CheckBounds, "`rucc_safety::lower`, into a call carrying the row that describes it"),
(Opcode::CheckLive, "`rucc_safety::lower`, the same call over the lifetime plane"),
(Opcode::CheckDeriv, "`rucc_safety::lower`, the same call where the pointer is computed"),
(Opcode::CapOf, "`rucc_safety::lower`, which removes it, since nothing reads it any more"),
];
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::AtomicRmw,
"a `lock` prefix, which is an operand form nothing here has",
"tamnd/rucc#311",
),
(Opcode::Cmpxchg, "the same, and a result that is a pair", "tamnd/rucc#311"),
(Opcode::Bitreverse, "a node nothing writes and nothing lowers", "tamnd/rucc#363"),
(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",
),
(
Opcode::CapLoad,
"a capability, whose runtime shape `spec/safe-memory/05-representation.md` decides",
"tamnd/rucc#428",
),
(Opcode::CapStore, "the same, and a store into the slot beside a pointer", "tamnd/rucc#428"),
(
Opcode::CapNull,
"the same, and it is whatever the representation says nothing is",
"tamnd/rucc#428",
),
(Opcode::CapNarrow, "the same, and arithmetic on the bounds it holds", "tamnd/rucc#428"),
(Opcode::CapRecover, "the same, and a read of the shadow planes", "tamnd/rucc#428"),
(Opcode::CheckType, "a read of the type plane, which is S5's", "tamnd/rucc#431"),
(Opcode::CheckInit, "the same, over the init plane, which is S5's too", "tamnd/rucc#431"),
(Opcode::CheckRace, "the same, over the epoch plane, which is S5's as well", "tamnd/rucc#431"),
(Opcode::MetaBegin, "a write over a range of the lifetime plane", "tamnd/rucc#428"),
(
Opcode::MetaEnd,
"the same write, with the version bumped past every capability",
"tamnd/rucc#428",
),
(Opcode::MetaType, "the same over the type plane, which is S5's", "tamnd/rucc#431"),
(Opcode::MetaInit, "the same over the init plane, which is S5's", "tamnd/rucc#431"),
(
Opcode::MetaTransfer,
"the same, and the state a range is in while a device owns it, which is S2's",
"tamnd/rucc#428",
),
(
Opcode::SafeRegionBegin,
"nothing at all, once the count document 10 section 10.2 asks for has been taken",
"tamnd/rucc#428",
),
(Opcode::SafeRegionEnd, "the same, which is to say nothing", "tamnd/rucc#428"),
];
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)] = &[
("sdiv.i8", "a narrow divide, which wants a range analysis before it can be narrowed", 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),
("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(_) | Test::Same(_) => 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);
}
}