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::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::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)] = &[
("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),
];
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> =
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);
}
}