use rucc_base::Interner;
use rucc_ir::{Func, Opcode};
use rucc_target::CallRegs;
use crate::{expand, quad, retry, switch, varargs, wide, widths};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Step {
Switches,
Retries,
Orderings,
Overflows,
Halves,
Widths,
Bytes,
Counts,
Quads,
Floats,
Bulk,
Rounds,
Varargs,
}
impl Step {
pub const GROUP: &'static [Self] = &[
Self::Switches,
Self::Retries,
Self::Orderings,
Self::Overflows,
Self::Halves,
Self::Widths,
Self::Bytes,
Self::Counts,
Self::Quads,
Self::Floats,
Self::Bulk,
Self::Rounds,
Self::Varargs,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Switches => "switches",
Self::Retries => "retries",
Self::Orderings => "orderings",
Self::Overflows => "overflows",
Self::Halves => "halves",
Self::Widths => "widths",
Self::Bytes => "bytes",
Self::Counts => "counts",
Self::Quads => "quads",
Self::Floats => "floats",
Self::Bulk => "bulk",
Self::Rounds => "rounds",
Self::Varargs => "varargs",
}
}
#[must_use]
pub const fn construct(self) -> &'static str {
match self {
Self::Switches => "a switch",
Self::Retries => "a read modify write with no instruction behind it",
Self::Orderings => "an ordered load or store",
Self::Overflows => "arithmetic that reports whether it overflowed",
Self::Halves => "an integer wider than a register",
Self::Widths => "an integer at a width the machine does not have",
Self::Bytes => "a byte reversal",
Self::Counts => "a bit count",
Self::Quads => "the quad float format",
Self::Floats => "a float constant, a negation or a conversion",
Self::Bulk => "a bulk copy or fill",
Self::Rounds => "a stack allocation whose size is not a multiple of the alignment",
Self::Varargs => "a variable argument list",
}
}
#[must_use]
pub const fn opcodes(self) -> &'static [Opcode] {
match self {
Self::Switches => &[Opcode::Switch],
Self::Retries => &[],
Self::Orderings => &[Opcode::AtomicLoad, Opcode::AtomicStore],
Self::Overflows => &[
Opcode::UAddOverflow,
Opcode::SAddOverflow,
Opcode::USubOverflow,
Opcode::SSubOverflow,
Opcode::UMulOverflow,
Opcode::SMulOverflow,
],
Self::Halves | Self::Widths | Self::Rounds => &[],
Self::Bytes => &[Opcode::Bswap],
Self::Counts => &[Opcode::Ctlz, Opcode::Cttz, Opcode::Ctpop],
Self::Quads => &[],
Self::Floats => &[
Opcode::FConst,
Opcode::FNeg,
Opcode::SIToFP,
Opcode::UIToFP,
Opcode::FPToSI,
Opcode::FPToUI,
],
Self::Bulk => &[Opcode::Memcpy, Opcode::Memset, Opcode::Memmove],
Self::Varargs => &[Opcode::VaArg, Opcode::VaObject, Opcode::VaCopy, Opcode::VaEnd],
}
}
#[must_use]
pub const fn whole_function(self) -> bool {
matches!(self, Self::Halves | Self::Widths)
}
fn run(self, func: &mut Func, names: &mut Interner, conv: &CallRegs) -> bool {
match self {
Self::Switches => switch::switches(func),
Self::Retries => retry::loops(func),
Self::Orderings => expand::orderings(func, conv.word),
Self::Overflows => expand::overflows(func),
Self::Halves => return wide::halves(func, names, conv),
Self::Widths => return widths::integers(func),
Self::Bytes => expand::bytes(func),
Self::Counts => expand::counts(func),
Self::Quads => quad::calls(func, names),
Self::Floats => expand::floats(func),
Self::Bulk => expand::bulk(func, names, conv.word),
Self::Rounds => expand::rounds(func, conv.stack_align),
Self::Varargs => varargs::lists(func, conv),
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Did {
pub step: Step,
pub found: usize,
pub left: usize,
pub before: usize,
pub after: usize,
pub untouched: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Ran {
pub did: Vec<Did>,
}
impl Ran {
#[must_use]
pub fn of(&self, step: Step) -> Did {
*self.did.iter().find(|did| did.step == step).expect("every step has an entry")
}
#[must_use]
pub fn render(&self, func: &str) -> String {
use std::fmt::Write;
let mut out = format!("lowering {func}\n");
for did in &self.did {
let _ = write!(
out,
" {:<10} {:>4} -> {:>4} insts",
did.step.name(),
did.before,
did.after
);
if did.step.whole_function() && !did.untouched {
let _ = write!(out, ", retyped every value at that width");
}
if did.found > 0 {
let _ = write!(out, ", found {}, left {}", did.found, did.left);
}
let _ = writeln!(out, " ({})", did.step.construct());
}
out
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Lowerings {
rows: Vec<(String, Ran)>,
wanted: bool,
}
impl Lowerings {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn asked(wanted: bool) -> Self {
Self { rows: Vec::new(), wanted }
}
#[must_use]
pub fn wanted(&self) -> bool {
self.wanted
}
pub fn record(&mut self, name: &str, ran: Ran) {
self.rows.push((name.to_owned(), ran));
}
pub fn merge(&mut self, other: &Self) {
self.rows.extend(other.rows.iter().cloned());
}
#[must_use]
pub fn functions(&self) -> usize {
self.rows.len()
}
#[must_use]
pub fn listing(&self) -> String {
let mut out = format!("# rucc lowering: {} functions\n", self.rows.len());
for (name, ran) in &self.rows {
out.push_str(&ran.render(name));
}
out
}
}
pub fn group(func: &mut Func, names: &mut Interner, conv: &CallRegs, counting: bool) -> Ran {
let mut ran = Ran::default();
for &step in Step::GROUP {
if !counting {
step.run(func, names, conv);
continue;
}
let (before, found) = tally(func, step);
let did = step.run(func, names, conv);
let (after, left) = tally(func, step);
ran.did.push(Did { step, found, left, before, after, untouched: !did });
}
ran
}
fn tally(func: &Func, step: Step) -> (usize, usize) {
let wanted = step.opcodes();
let (mut all, mut mine) = (0, 0);
for block in func.blocks() {
for inst in func.insts(block) {
all += 1;
if wanted.contains(&func[inst].opcode) {
mine += 1;
}
}
}
(all, mine)
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Builder, Extra, Flags, Float, Func, InstData, MemInfo, MemOrder, Opcode, Restrict,
Signature, Type, Value,
};
use rucc_target::x86_64;
use super::{Lowerings, Ran, Step, group};
fn one(
params: &[Type],
returns: &[Type],
body: impl FnOnce(&mut Builder<'_>, &[Value]),
) -> (Interner, Func) {
let mut names = Interner::new();
let mut func = Func::new(
names.intern("f"),
Signature::new().with_params(params).with_returns(returns),
);
let entry = func.create_block();
let args: Vec<_> = params.iter().map(|&ty| func.append_param(entry, ty)).collect();
let mut build = Builder::new(&mut func, entry);
body(&mut build, &args);
(names, func)
}
fn run(func: &mut Func, names: &mut Interner) -> Ran {
group(func, names, &x86_64::SYSV, true)
}
fn i32() -> Type {
Type::int(32)
}
#[test]
fn the_group_is_the_passes_the_pipeline_used_to_call_one_line_at_a_time() {
let names: Vec<&str> = Step::GROUP.iter().map(|step| step.name()).collect();
assert_eq!(
names,
[
"switches",
"retries",
"orderings",
"overflows",
"halves",
"widths",
"bytes",
"counts",
"quads",
"floats",
"bulk",
"rounds",
"varargs",
]
);
}
#[test]
fn every_step_says_what_it_is_for_and_no_two_say_the_same_thing() {
let mut names: Vec<&str> = Step::GROUP.iter().map(|step| step.name()).collect();
let mut constructs: Vec<&str> = Step::GROUP.iter().map(|step| step.construct()).collect();
assert!(constructs.iter().all(|construct| !construct.is_empty()));
for list in [&mut names, &mut constructs] {
let was = list.len();
list.sort_unstable();
list.dedup();
assert_eq!(list.len(), was, "two steps say the same thing");
}
}
#[test]
fn a_function_with_nothing_in_it_leaves_every_step_with_nothing_to_say() {
let (mut names, mut func) = one(&[], &[], |build, _| {
build.ret(&[]);
});
let ran = run(&mut func, &mut names);
assert_eq!(ran.did.len(), Step::GROUP.len());
assert!(ran.did.iter().all(|did| did.found == 0 && did.before == did.after));
}
#[test]
fn nothing_in_the_group_is_left_out_of_the_record() {
let (mut names, mut func) = one(&[], &[], |build, _| {
build.ret(&[]);
});
let ran = run(&mut func, &mut names);
let ordered: Vec<Step> = ran.did.iter().map(|did| did.step).collect();
assert_eq!(ordered, Step::GROUP);
}
#[test]
fn a_byte_reversal_does_not_survive_the_group() {
let (mut names, mut func) = one(&[i32()], &[i32()], |build, args| {
let swapped = build.unary(Opcode::Bswap, args[0], i32());
build.ret(&[swapped]);
});
let ran = run(&mut func, &mut names);
let did = ran.of(Step::Bytes);
assert_eq!(did.found, 1);
assert_eq!(did.left, 0);
assert!(did.after > did.before, "one instruction became several");
}
#[test]
fn a_bit_count_does_not_survive_the_group() {
let (mut names, mut func) = one(&[i32()], &[i32()], |build, args| {
let ones = build.unary(Opcode::Ctpop, args[0], i32());
build.ret(&[ones]);
});
let ran = run(&mut func, &mut names);
assert_eq!(ran.of(Step::Counts).found, 1);
assert_eq!(ran.of(Step::Counts).left, 0);
}
#[test]
fn a_float_negation_does_not_survive_the_group() {
let f64 = Type::float(Float::F64);
let (mut names, mut func) = one(&[f64], &[f64], |build, args| {
let negated = build.unary(Opcode::FNeg, args[0], f64);
build.ret(&[negated]);
});
let ran = run(&mut func, &mut names);
assert_eq!(ran.of(Step::Floats).found, 1);
assert_eq!(ran.of(Step::Floats).left, 0);
}
#[test]
fn an_ordered_load_does_not_survive_the_group() {
let i64 = Type::int(64);
let (mut names, mut func) = one(&[Type::PTR], &[i64], |build, args| {
let info = MemInfo {
size: 8,
align: 8,
order: MemOrder::SeqCst,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let value = build.atomic_load(i64, args[0], info, Flags::NONE);
build.ret(&[value]);
});
let ran = run(&mut func, &mut names);
assert_eq!(ran.of(Step::Orderings).found, 1);
assert_eq!(ran.of(Step::Orderings).left, 0);
}
#[test]
fn nothing_the_group_names_an_opcode_for_is_still_there_afterwards() {
for step in Step::GROUP {
let Some((mut names, mut func)) = holding(*step) else {
continue;
};
let ran = run(&mut func, &mut names);
let did = ran.of(*step);
assert_eq!(did.found, 1, "{}: the construct was not built", step.name());
assert_eq!(did.left, 0, "{}: the construct survived the group", step.name());
}
}
fn holding(step: Step) -> Option<(Interner, Func)> {
let i32 = i32();
let i64 = Type::int(64);
let f64 = Type::float(Float::F64);
Some(match step {
Step::Bytes => one(&[i32], &[i32], |build, args| {
let swapped = build.unary(Opcode::Bswap, args[0], i32);
build.ret(&[swapped]);
}),
Step::Counts => one(&[i32], &[i32], |build, args| {
let ones = build.unary(Opcode::Ctlz, args[0], i32);
build.ret(&[ones]);
}),
Step::Floats => one(&[], &[f64], |build, _| {
let k = build.fconst(f64, 0x3ff8_0000_0000_0000);
build.ret(&[k]);
}),
Step::Orderings => one(&[Type::PTR], &[i64], |build, args| {
let info = MemInfo {
size: 8,
align: 8,
order: MemOrder::SeqCst,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let value = build.atomic_load(i64, args[0], info, Flags::NONE);
build.ret(&[value]);
}),
Step::Overflows => one(&[i32, i32], &[i32], |build, args| {
let (sum, _) = build.checked(Opcode::UAddOverflow, args[0], args[1]);
build.ret(&[sum]);
}),
Step::Bulk => one(&[Type::PTR, Type::PTR], &[], |build, args| {
let info = MemInfo {
size: 16,
align: 8,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let mem = build.func().add_mem(info);
let operands = build.func().push_values(&[args[0], args[1]]);
build.inst(
InstData {
args: operands,
extra: Extra::Mem(mem),
..InstData::new(Opcode::Memcpy)
},
&[],
);
build.ret(&[]);
}),
_ => return None,
})
}
#[test]
fn a_run_that_did_not_ask_for_the_dump_still_lowers_and_counts_nothing() {
let build = |build: &mut Builder<'_>, args: &[Value]| {
let swapped = build.unary(Opcode::Bswap, args[0], i32());
build.ret(&[swapped]);
};
let (mut names, mut func) = one(&[i32()], &[i32()], build);
let quiet = group(&mut func, &mut names, &x86_64::SYSV, false);
assert!(quiet.did.is_empty(), "nothing was counted");
assert_eq!(super::tally(&func, Step::Bytes), (super::tally(&func, Step::Bytes).0, 0));
let (mut names, mut func) = one(&[i32()], &[i32()], build);
let loud = group(&mut func, &mut names, &x86_64::SYSV, true);
assert_eq!(loud.of(Step::Bytes).left, 0);
assert_eq!(
loud.did.last().expect("thirteen of them").after,
super::tally(&func, Step::Bytes).0
);
}
#[test]
fn nothing_is_recorded_for_a_run_that_did_not_ask() {
let mut quiet = Lowerings::new();
assert!(!quiet.wanted());
quiet.record("f", Ran::default());
assert_eq!(quiet.functions(), 1, "recording still works if somebody does it anyway");
let asked = Lowerings::asked(true);
assert!(asked.wanted());
assert_eq!(asked.listing(), "# rucc lowering: 0 functions\n");
}
#[test]
fn the_dump_names_every_step_whether_it_fired_or_not() {
let (mut names, mut func) = one(&[i32()], &[i32()], |build, args| {
let swapped = build.unary(Opcode::Bswap, args[0], i32());
build.ret(&[swapped]);
});
let ran = run(&mut func, &mut names);
let text = ran.render("f");
assert!(text.starts_with("lowering f\n"), "{text}");
for step in Step::GROUP {
assert!(text.contains(step.name()), "{} is missing from {text}", step.name());
}
assert!(text.contains("found 1, left 0"), "{text}");
assert_eq!(text.lines().count(), Step::GROUP.len() + 1);
}
#[test]
fn only_the_two_steps_that_retype_a_whole_function_ever_say_they_touched_nothing() {
assert_eq!(
Step::GROUP.iter().filter(|step| step.whole_function()).copied().collect::<Vec<_>>(),
[Step::Halves, Step::Widths]
);
for step in Step::GROUP {
if step.whole_function() {
assert!(step.opcodes().is_empty(), "{} counts opcodes", step.name());
}
}
}
#[test]
fn an_instruction_nothing_in_the_group_is_about_is_left_exactly_where_it_was() {
let (mut names, mut func) = one(&[i32()], &[i32()], |build, args| {
let seven = build.iconst(i32(), 7);
let sum = build.binary(Opcode::Add, args[0], seven, Flags::NONE);
build.ret(&[sum]);
});
let before = super::tally(&func, Step::Rounds).0;
let ran = run(&mut func, &mut names);
assert_eq!(super::tally(&func, Step::Rounds).0, before);
assert!(ran.did.iter().all(|did| did.found == 0));
}
}