use rucc_ir::Opcode;
use crate::coverage::{GAPS, NAMES};
use crate::lowering::Step;
use crate::select::Table;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lowering {
Group(Step),
Hand(&'static str),
}
impl Lowering {
#[must_use]
pub const fn where_(self) -> &'static str {
match self {
Lowering::Group(step) => step.name(),
Lowering::Hand(where_) => where_,
}
}
}
#[derive(Debug, Clone)]
pub struct Row {
pub opcode: Opcode,
pub name: &'static str,
pub rule: bool,
pub lowering: Option<Lowering>,
pub libcall: Option<&'static str>,
pub nothing: Option<(&'static str, &'static str)>,
}
impl Row {
#[must_use]
pub const fn answered(&self) -> bool {
self.rule || self.lowering.is_some() || self.libcall.is_some()
}
}
pub static HAND: &[(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::StackSave, "`crate::lower`, as a move out of the stack pointer"),
(Opcode::StackRestore, "`crate::lower`, the same move the other way round"),
(Opcode::GlobalAddr, "`crate::lower`, a `lea` off the instruction pointer with a name on it"),
(Opcode::BlockAddr, "`crate::lower`, the same `lea` against a label of this function"),
(Opcode::ThreadPointer, "`crate::lower`, as the load through `%fs` at zero that reads it"),
(Opcode::Prefetch, "`crate::lower`, as one of the four `prefetch` instructions"),
(Opcode::Trap, "`crate::lower`, as the `ud2` the program stops on"),
(Opcode::FrameAddress, "`crate::lower`, as the walk up the saved frame pointers"),
(Opcode::ReturnAddress, "`crate::lower`, as the same walk with one load at the end of 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::IndirectBr, "`crate::lower`, as the jump through the register that holds the address"),
(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::InlineAsm,
"`crate::lower`, as the places its operands share and the instructions its template names",
),
(
Opcode::Fence,
"`crate::lower`, as an `mfence` at the strongest ordering and nothing below it",
),
(
Opcode::Cmpxchg,
"`crate::lower`, as a locked compare and exchange and the byte that reads its answer",
),
(
Opcode::AtomicRmw,
"`crate::lower`, as an exchange or a locked add, and `crate::retry` for the eight with no \
instruction, with the two on floating values refused",
),
(Opcode::VaStart, "`crate::varargs`, which writes the register save area the ABI describes"),
(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::CheckType, "`rucc_safety::lower`, the same call, carrying the type asked about"),
(
Opcode::CheckInit,
"`rucc_safety::lower`, the same call over the init plane, carrying no type",
),
(Opcode::CheckRace, "`rucc_safety::lower`, the same call over the epoch plane"),
(Opcode::MetaType, "`rucc_safety::lower`, into the call that records what a store stored"),
(Opcode::MetaTypeCopy, "`rucc_safety::lower`, the same call over the range a copy read"),
(Opcode::MetaInit, "`rucc_safety::lower`, into the call that says a store wrote a range"),
(Opcode::MetaInitCopy, "`rucc_safety::lower`, the same call over the range a copy read"),
(Opcode::MetaEpoch, "`rucc_safety::lower`, into the call that says which thread stored"),
(
Opcode::MetaRelease,
"`rucc_safety::lower`, into the call that publishes this thread's clock at an atomic",
),
(Opcode::MetaAcquire, "`rucc_safety::lower`, into the call that takes the other end of it"),
(
Opcode::MetaFenceRelease,
"`rucc_safety::lower`, into the call that publishes this thread's clock to everyone",
),
(
Opcode::MetaFenceAcquire,
"`rucc_safety::lower`, into the call that takes what any release fence published",
),
(
Opcode::CheckRestrictRead,
"`rucc_safety::lower`, into the call that asks what the block has already reached",
),
(Opcode::CheckRestrictWrite, "`rucc_safety::lower`, the same call, saying it wrote"),
(Opcode::RestrictEnter, "`rucc_safety::lower`, into the call that opens the block's record"),
(Opcode::RestrictLeave, "`rucc_safety::lower`, into the call that closes it again"),
(Opcode::SafeRegionBegin, "`rucc_safety::lower`, into nothing, once the count has been taken"),
(Opcode::SafeRegionEnd, "`rucc_safety::lower`, the same, which is to say nothing"),
(Opcode::CapExtent, "`rucc_safety::lower`, into a call that asks rather than one that judges"),
(Opcode::CapExtentBack, "`rucc_safety::lower`, the same call about the bytes below an address"),
(Opcode::CapOf, "`rucc_safety::slot`, into the header read at an allocation site or the walk"),
(Opcode::CapNull, "`rucc_safety::slot`, into a frame slot with the bottom capability in it"),
(Opcode::CapStore, "`rucc_safety::slot`, into the call that writes one into the aux plane"),
(Opcode::CapLoad, "`rucc_safety::slot`, into the call that reads one back out again"),
(Opcode::CapNarrow, "`rucc_safety::slot`, into the call that moves the range in"),
(Opcode::CapRecover, "`rucc_safety::slot`, into the call that walks the planes for one"),
(Opcode::CapPublish, "`rucc_safety::frame`, into the frame a call hands its callee"),
(Opcode::CapClear, "`rucc_safety::frame`, into the call that says there is no frame"),
(Opcode::CapArg, "`rucc_safety::frame`, into the read of the frame the caller published"),
(
Opcode::CapYield,
"`rucc_safety::frame`, into the write of the frame the caller is waiting on",
),
(Opcode::CapResult, "`rucc_safety::frame`, into the read of what the callee left behind"),
(Opcode::Expect, "`rucc_opt::expect`, which moves the hint onto the branch and removes it"),
];
pub static LIBCALLS: &[(Opcode, &str, &str)] = &[
(Opcode::FAdd, "f128", "__addtf3"),
(Opcode::FSub, "f128", "__subtf3"),
(Opcode::FMul, "f128", "__multf3"),
(Opcode::FDiv, "f128", "__divtf3"),
(Opcode::FNeg, "f128", "__negtf2"),
(Opcode::FCmp, "oeq.f128", "__eqtf2"),
(Opcode::FCmp, "une.f128", "__netf2"),
(Opcode::FCmp, "olt.f128", "__lttf2"),
(Opcode::FCmp, "ole.f128", "__letf2"),
(Opcode::FCmp, "ogt.f128", "__gttf2"),
(Opcode::FCmp, "oge.f128", "__getf2"),
(Opcode::FCmp, "uno.f128", "__unordtf2"),
(Opcode::FPExt, "f32.f128", "__extendsftf2"),
(Opcode::FPExt, "f64.f128", "__extenddftf2"),
(Opcode::FPTrunc, "f128.f32", "__trunctfsf2"),
(Opcode::FPTrunc, "f128.f64", "__trunctfdf2"),
(Opcode::SIToFP, "i32.f128", "__floatsitf"),
(Opcode::SIToFP, "i64.f128", "__floatditf"),
(Opcode::UIToFP, "i32.f128", "__floatunsitf"),
(Opcode::UIToFP, "i64.f128", "__floatunditf"),
(Opcode::FPToSI, "f128.i32", "__fixtfsi"),
(Opcode::FPToSI, "f128.i64", "__fixtfdi"),
(Opcode::FPToUI, "f128.i32", "__fixunstfsi"),
(Opcode::FPToUI, "f128.i64", "__fixunstfdi"),
(Opcode::UDiv, "i128", "__udivti3"),
(Opcode::SDiv, "i128", "__divti3"),
(Opcode::URem, "i128", "__umodti3"),
(Opcode::SRem, "i128", "__modti3"),
(Opcode::SIToFP, "i128.f32", "__floattisf"),
(Opcode::SIToFP, "i128.f64", "__floattidf"),
(Opcode::SIToFP, "i128.f128", "__floattitf"),
(Opcode::UIToFP, "i128.f32", "__floatuntisf"),
(Opcode::UIToFP, "i128.f64", "__floatuntidf"),
(Opcode::UIToFP, "i128.f128", "__floatuntitf"),
(Opcode::FPToSI, "f32.i128", "__fixsfti"),
(Opcode::FPToSI, "f64.i128", "__fixdfti"),
(Opcode::FPToSI, "f128.i128", "__fixtfti"),
(Opcode::FPToUI, "f32.i128", "__fixunssfti"),
(Opcode::FPToUI, "f64.i128", "__fixunsdfti"),
(Opcode::FPToUI, "f128.i128", "__fixunstfti"),
(Opcode::Memcpy, "big", "memcpy"),
(Opcode::Memset, "big", "memset"),
(Opcode::Memmove, "any", "memmove"),
];
#[must_use]
pub fn libcall(opcode: Opcode, mode: &str) -> Option<&'static str> {
LIBCALLS
.iter()
.find(|&&(at, spelled, _)| at == opcode && spelled == mode)
.map(|&(_, _, name)| name)
}
#[must_use]
pub fn lowering(opcode: Opcode) -> Option<Lowering> {
for &step in Step::GROUP {
if step.opcodes().contains(&opcode) {
return Some(Lowering::Group(step));
}
}
HAND.iter().find(|&&(at, _)| at == opcode).map(|&(_, where_)| Lowering::Hand(where_))
}
#[must_use]
pub fn rows(table: &Table) -> Vec<Row> {
let patterns = pattern_heads(table);
let named = rucc_ir::term::heads();
let mut out = Vec::with_capacity(named.len() + LIBCALLS.len() + 64);
for &(opcode, name) in &named {
out.push(Row {
opcode,
name,
rule: patterns.contains(&name),
lowering: lowering(opcode),
libcall: None,
nothing: NAMES
.iter()
.find(|&&(at, ..)| at == name)
.map(|&(_, why, issue)| (why, issue)),
});
}
for &(opcode, mode, call) in LIBCALLS {
out.push(Row {
opcode,
name: mode,
rule: false,
lowering: lowering(opcode),
libcall: Some(call),
nothing: None,
});
}
for opcode in Opcode::all() {
if named.iter().any(|&(at, _)| at == opcode) {
continue;
}
if LIBCALLS.iter().any(|&(at, ..)| at == opcode) {
continue;
}
out.push(Row {
opcode,
name: opcode.name(),
rule: false,
lowering: lowering(opcode),
libcall: None,
nothing: GAPS
.iter()
.find(|&&(at, ..)| at == opcode)
.map(|&(_, why, issue)| (why, issue)),
});
}
out
}
pub(crate) 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.heads.iter().map(|&(head, ..)| head).collect();
found.dedup();
found
}
#[cfg(test)]
mod tests {
use super::*;
use crate::select::x86_64::TABLE;
#[test]
fn every_row_has_at_least_one_answer_or_says_why_it_has_none() {
let mut unanswered = Vec::new();
for row in rows(&TABLE) {
if row.answered() || row.nothing.is_some() {
continue;
}
unanswered.push(row.name);
}
assert!(
unanswered.is_empty(),
"no rule lowers these, nothing rewrites them, no runtime function stands for them and \
nothing says why: {unanswered:?}"
);
}
#[test]
fn a_row_with_no_answer_names_the_issue_that_gives_it_one() {
for row in rows(&TABLE) {
let Some((why, issue)) = row.nothing else { continue };
assert!(
!row.answered(),
"`{}` is {why} and is also answered, so the entry is stale and {issue} may be \
closed",
row.name
);
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 a_rule_at_a_name_something_rewrites_by_hand_could_never_fire() {
for row in rows(&TABLE) {
let Some(Lowering::Hand(where_)) = row.lowering else { continue };
assert!(
!row.rule,
"`{}` is rewritten by {where_} before selection, so the rule written at it can \
never fire",
row.name
);
}
}
#[test]
fn an_operation_the_machine_has_and_a_lowering_names_is_allowed_both() {
let both: Vec<&str> = rows(&TABLE)
.iter()
.filter(|row| row.rule && matches!(row.lowering, Some(Lowering::Group(_))))
.map(|row| row.name)
.collect();
assert!(
both.iter().any(|name| name.starts_with("sitofp.")),
"a signed conversion is what `crate::expand` walks away from when the machine has the \
instruction, and the table should show both answers: {both:?}"
);
}
#[test]
fn the_lowering_column_is_the_group_saying_what_it_is_about() {
for &step in Step::GROUP {
for &opcode in step.opcodes() {
assert_eq!(
lowering(opcode),
Some(Lowering::Group(step)),
"`{}` is named by `{}` and the table says otherwise",
opcode.name(),
step.name()
);
}
}
}
#[test]
fn nothing_the_group_names_is_also_written_down_by_hand() {
for &(opcode, where_) in HAND {
for &step in Step::GROUP {
assert!(
!step.opcodes().contains(&opcode),
"`{}` is named by `{}` and the hand written list says it is lowered by {where_}",
opcode.name(),
step.name()
);
}
}
}
#[test]
fn no_two_rows_answer_for_the_same_operation_at_the_same_mode() {
let mut seen: Vec<(Opcode, &str)> = Vec::new();
for &(opcode, mode, call) in LIBCALLS {
assert!(
!seen.contains(&(opcode, mode)),
"`{}` at `{mode}` is answered twice, and the second answer is {call}",
opcode.name()
);
seen.push((opcode, mode));
}
}
#[test]
fn the_passes_ask_for_a_call_by_the_operation_and_the_mode() {
assert_eq!(libcall(Opcode::SDiv, "i128"), Some("__divti3"));
assert_eq!(libcall(Opcode::FAdd, "f128"), Some("__addtf3"));
assert_eq!(libcall(Opcode::SIToFP, "i128.f64"), Some("__floattidf"));
assert_eq!(libcall(Opcode::Memmove, "any"), Some("memmove"));
assert_eq!(libcall(Opcode::SDiv, "i64"), None, "a divide the machine has is not a call");
assert_eq!(libcall(Opcode::Add, "i128"), None, "a wide add is two adds and not a call");
}
#[test]
fn a_runtime_function_is_spelled_the_way_the_runtime_spells_it() {
for &(opcode, mode, call) in LIBCALLS {
let library = matches!(opcode, Opcode::Memcpy | Opcode::Memset | Opcode::Memmove);
assert_eq!(
call.starts_with("__"),
!library,
"`{call}` is a {} function and is not spelled like one",
if library { "C library" } else { "compiler runtime" }
);
assert!(
!mode.is_empty() && mode.is_ascii(),
"`{call}` answers for a mode with no name"
);
}
}
#[test]
fn the_table_is_one_row_per_operation_and_mode() {
let rows = rows(&TABLE);
assert!(
rows.len() > Opcode::all().count(),
"an operation with more than one mode is more than one row, so there are more rows \
than there are opcodes: {} rows and {} opcodes",
rows.len(),
Opcode::all().count()
);
let with_rule = rows.iter().filter(|row| row.rule).count();
let with_lowering = rows.iter().filter(|row| row.lowering.is_some()).count();
let with_libcall = rows.iter().filter(|row| row.libcall.is_some()).count();
assert!(with_rule > 0 && with_lowering > 0 && with_libcall > 0);
assert_eq!(with_libcall, LIBCALLS.len());
println!(
"rucc-codegen: {} rows, {with_rule} by rule, {with_lowering} by lowering, \
{with_libcall} by a call to the runtime",
rows.len()
);
}
#[test]
fn every_opcode_the_ir_has_is_in_the_table() {
let rows = rows(&TABLE);
for opcode in Opcode::all() {
assert!(
rows.iter().any(|row| row.opcode == opcode),
"`{}` has no row, so nothing says what this target does about it",
opcode.name()
);
}
}
#[test]
fn a_hand_written_entry_names_where_the_answer_is() {
for &(opcode, where_) in HAND {
assert!(
where_.contains('`') || where_.starts_with("nothing"),
"the entry for `{}` says {where_}, which names no module",
opcode.name()
);
}
assert_eq!(Lowering::Hand("`crate::abi`, and so on").where_(), "`crate::abi`, and so on");
assert_eq!(Lowering::Group(Step::Bytes).where_(), "bytes");
}
}