use core::fmt;
use core::fmt::Write as _;
use rucc_ir::Opcode;
use rucc_target::Arch;
use crate::capability::{self, pattern_heads};
use crate::select::Table;
use crate::term;
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::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::Bitreverse, "a node nothing writes and nothing lowers", "tamnd/rucc#363"),
(Opcode::TailCall, "a terminator nothing writes and nothing lowers", "tamnd/rucc#365"),
(Opcode::MetaBegin, "a write over a range of the lifetime plane", "tamnd/rucc#856"),
(
Opcode::MetaEnd,
"the same write, with the version bumped past every capability",
"tamnd/rucc#856",
),
(
Opcode::MetaTransfer,
"the same, and the state a range is in while a device owns it, which is S2's",
"tamnd/rucc#856",
),
];
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",
"split into two halves before selection, except a division",
"tamnd/rucc#351",
),
(
"eighty bits",
"a long double is on the x87 stack and no rule is about that stack",
"tamnd/rucc#326",
),
(
"a hundred and twenty eight bits of float",
"turned into a call before selection, except a conditional move and the conversions \
against an integer that wide",
"tamnd/rucc#1064",
),
(
"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)] = &[];
#[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> =
Opcode::all().filter(|&opcode| capability::lowering(opcode).is_some()).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)
&& !capability::LIBCALLS.iter().any(|&(at, ..)| at == *opcode)
})
.collect();
Report {
source: table.source,
opcodes: Opcode::all().count(),
by_rule,
names,
uncovered,
deferred,
unreachable,
elsewhere,
gaps,
unaccounted,
}
}
#[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, nothing rewrites them before selection, no runtime function \
stands for them and `GAPS` does not say why: {:?}",
report.unaccounted
);
}
#[test]
fn an_entry_a_rule_now_covers_is_a_stale_entry() {
let report = report(&TABLE);
for &(opcode, where_) in capability::HAND {
assert!(
!report.by_rule.contains(&opcode),
"`{}` is lowered by a rule now, so the `HAND` 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);
}
}