use std::cmp::Ordering;
use std::collections::HashMap;
use rucc_base::Interner;
use rucc_ir::{
CallInfo, Def, Extra, Flags, FloatPred, Func, Imm, Inst, InstData, IntPred, MemInfo, MemOrder,
Opcode, Signature, Type, Value,
};
pub fn orderings(func: &mut Func, word: u32) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
match func[inst].opcode {
Opcode::AtomicLoad => relaxed(func, inst, Opcode::Load, word),
Opcode::AtomicStore => relaxed(func, inst, Opcode::Store, word),
_ => {}
}
}
}
fn relaxed(func: &mut Func, inst: Inst, plain: Opcode, word: u32) {
let Extra::Mem(mem) = func[inst].extra else { return };
let info = func[mem];
let ty = match plain {
Opcode::Store => match func[func[inst].args].first() {
Some(&value) => func[value].ty,
None => return,
},
_ => produced(func, inst),
};
if !indivisible(ty, info, word) {
return;
}
let unordered = MemInfo { order: MemOrder::NotAtomic, ..info };
if plain == Opcode::Store && info.order == MemOrder::SeqCst {
let [value, addr] = func[func[inst].args] else { return };
write(func, inst, value, addr, unordered);
let none = func.push_values(&[]);
let data = &mut func[inst];
data.opcode = Opcode::Fence;
data.args = none;
data.extra = Extra::Order(MemOrder::SeqCst);
data.flags = data.flags.intersection(Flags::legal_on(Opcode::Fence));
return;
}
let plainly = func.add_mem(unordered);
let data = &mut func[inst];
data.opcode = plain;
data.extra = Extra::Mem(plainly);
data.flags = data.flags.intersection(Flags::legal_on(plain));
}
fn indivisible(ty: Type, info: MemInfo, word: u32) -> bool {
let bytes = if ty.is_ptr() { word } else { ty.bits().div_ceil(8) };
ty.is_scalar() && bytes.is_power_of_two() && bytes <= word && info.align >= bytes
}
pub fn floats(func: &mut Func) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
match func[inst].opcode {
Opcode::FConst => constant(func, inst),
Opcode::FNeg => negate(func, inst),
Opcode::SIToFP | Opcode::UIToFP => widen_then_convert(func, inst),
Opcode::FPToSI | Opcode::FPToUI => convert_then_narrow(func, inst),
_ => {}
}
}
}
fn constant(func: &mut Func, inst: Inst) {
let ty = produced(func, inst);
let Extra::Imm(imm) = func[inst].extra else { return };
if !ty.is_float() || !ty.is_scalar() || ty.bits() > 64 {
return;
}
let int = Type::int(ty.bits());
let bits = func[imm].bits();
let spelled = ahead_const(func, inst, Imm::int(bits as i128, int), int);
becomes(func, inst, Opcode::Bitcast, &[spelled]);
}
fn negate(func: &mut Func, inst: Inst) {
let ty = produced(func, inst);
let Some(&arg) = func[func[inst].args].first() else { return };
if !ty.is_float() || !ty.is_scalar() || ty.bits() > 64 {
return;
}
let int = Type::int(ty.bits());
let bits = ahead(func, inst, Opcode::Bitcast, &[arg], int);
let mask = ahead_const(func, inst, Imm::int(1i128 << (ty.bits() - 1), int), int);
let flipped = ahead(func, inst, Opcode::Xor, &[bits, mask], int);
becomes(func, inst, Opcode::Bitcast, &[flipped]);
}
fn widen_then_convert(func: &mut Func, inst: Inst) {
let signed = func[inst].opcode == Opcode::SIToFP;
let Some(&arg) = func[func[inst].args].first() else { return };
let from = func[arg].ty;
if !from.is_int() || !from.is_scalar() {
return;
}
let Some(width) = holder(from.bits(), signed) else {
from_unsigned_word(func, inst, arg, from);
return;
};
if width == from.bits() {
return;
}
let widen = if signed { Opcode::SExt } else { Opcode::ZExt };
let wide = ahead(func, inst, widen, &[arg], Type::int(width));
becomes(func, inst, Opcode::SIToFP, &[wide]);
}
fn convert_then_narrow(func: &mut Func, inst: Inst) {
let signed = func[inst].opcode == Opcode::FPToSI;
let ty = produced(func, inst);
let Some(&arg) = func[func[inst].args].first() else { return };
if !ty.is_int() || !ty.is_scalar() {
return;
}
let Some(width) = holder(ty.bits(), signed) else {
to_unsigned_word(func, inst, arg, ty);
return;
};
if width == ty.bits() {
return;
}
let wide = ahead(func, inst, Opcode::FPToSI, &[arg], Type::int(width));
becomes(func, inst, Opcode::Trunc, &[wide]);
}
fn from_unsigned_word(func: &mut Func, inst: Inst, arg: Value, from: Type) {
let ty = produced(func, inst);
if !ty.is_float() || !ty.is_scalar() {
return;
}
if ty.bits() > 64 {
from_unsigned_word_wide(func, inst, arg, from);
return;
}
let spread = spread_top_bit(func, inst, arg, from);
let one = ahead_const(func, inst, Imm::int(1, from), from);
let lost = ahead(func, inst, Opcode::And, &[arg, one], from);
let half = ahead(func, inst, Opcode::LShr, &[arg, one], from);
let odd = ahead(func, inst, Opcode::Or, &[half, lost], from);
let differ = ahead(func, inst, Opcode::Xor, &[arg, odd], from);
let taken = ahead(func, inst, Opcode::And, &[differ, spread], from);
let source = ahead(func, inst, Opcode::Xor, &[arg, taken], from);
let converted = ahead(func, inst, Opcode::SIToFP, &[source], ty);
let bits = Type::int(ty.bits());
let narrow = same_width(func, inst, spread, from, bits);
let raw = ahead(func, inst, Opcode::Bitcast, &[converted], bits);
let again = ahead(func, inst, Opcode::And, &[raw, narrow], bits);
let addend = ahead(func, inst, Opcode::Bitcast, &[again], ty);
becomes(func, inst, Opcode::FAdd, &[converted, addend]);
}
fn to_unsigned_word(func: &mut Func, inst: Inst, arg: Value, ty: Type) {
let from = func[arg].ty;
if !from.is_float() || !from.is_scalar() {
return;
}
if from.bits() > 64 {
to_unsigned_word_wide(func, inst, arg, ty);
return;
}
let bits = Type::int(from.bits());
let pattern = Imm::int(half_the_range(from.bits()), bits);
let spelled = ahead_const(func, inst, pattern, bits);
let half = ahead(func, inst, Opcode::Bitcast, &[spelled], from);
let over = ahead_cmp(func, inst, Opcode::FCmp, Extra::FloatPred(FloatPred::Oge), &[arg, half]);
let wide = ahead(func, inst, Opcode::ZExt, &[over], bits);
let zero = ahead_const(func, inst, Imm::int(0, bits), bits);
let spread = ahead(func, inst, Opcode::Sub, &[zero, wide], bits);
let amount = ahead(func, inst, Opcode::And, &[spread, spelled], bits);
let taken = ahead(func, inst, Opcode::Bitcast, &[amount], from);
let under = ahead(func, inst, Opcode::FSub, &[arg, taken], from);
let low = ahead(func, inst, Opcode::FPToSI, &[under], ty);
let again = ahead(func, inst, Opcode::ZExt, &[over], ty);
let up = ahead_const(func, inst, Imm::int(i128::from(ty.bits() - 1), ty), ty);
let top = ahead(func, inst, Opcode::Shl, &[again, up], ty);
becomes(func, inst, Opcode::Xor, &[low, top]);
}
fn from_unsigned_word_wide(func: &mut Func, inst: Inst, arg: Value, from: Type) {
let ty = produced(func, inst);
let zero = ahead_const(func, inst, Imm::int(0, from), from);
let over = ahead_cmp(func, inst, Opcode::ICmp, Extra::IntPred(IntPred::Slt), &[arg, zero]);
let signed = ahead(func, inst, Opcode::SIToFP, &[arg], ty);
let range = ahead_float(func, inst, two_to_the(64), ty);
let flag = flag_as_float(func, inst, over, ty);
let addend = ahead(func, inst, Opcode::FMul, &[range, flag], ty);
becomes(func, inst, Opcode::FAdd, &[signed, addend]);
}
fn to_unsigned_word_wide(func: &mut Func, inst: Inst, arg: Value, ty: Type) {
let from = func[arg].ty;
let half = ahead_float(func, inst, two_to_the(63), from);
let over = ahead_cmp(func, inst, Opcode::FCmp, Extra::FloatPred(FloatPred::Oge), &[arg, half]);
let flag = flag_as_float(func, inst, over, from);
let taken = ahead(func, inst, Opcode::FMul, &[half, flag], from);
let under = ahead(func, inst, Opcode::FSub, &[arg, taken], from);
let low = ahead(func, inst, Opcode::FPToSI, &[under], ty);
let again = ahead(func, inst, Opcode::ZExt, &[over], ty);
let up = ahead_const(func, inst, Imm::int(i128::from(ty.bits() - 1), ty), ty);
let top = ahead(func, inst, Opcode::Shl, &[again, up], ty);
becomes(func, inst, Opcode::Xor, &[low, top]);
}
fn flag_as_float(func: &mut Func, inst: Inst, cond: Value, ty: Type) -> Value {
let wide = ahead(func, inst, Opcode::ZExt, &[cond], Type::int(64));
ahead(func, inst, Opcode::SIToFP, &[wide], ty)
}
const fn two_to_the(power: u32) -> u128 {
((0x3fff + power as u128) << 64) | 0x8000_0000_0000_0000
}
fn spread_top_bit(func: &mut Func, inst: Inst, arg: Value, ty: Type) -> Value {
let zero = ahead_const(func, inst, Imm::int(0, ty), ty);
let set = ahead_cmp(func, inst, Opcode::ICmp, Extra::IntPred(IntPred::Slt), &[arg, zero]);
let wide = ahead(func, inst, Opcode::ZExt, &[set], ty);
ahead(func, inst, Opcode::Sub, &[zero, wide], ty)
}
fn same_width(func: &mut Func, inst: Inst, value: Value, from: Type, to: Type) -> Value {
match to.bits().cmp(&from.bits()) {
Ordering::Equal => value,
Ordering::Less => ahead(func, inst, Opcode::Trunc, &[value], to),
Ordering::Greater => ahead(func, inst, Opcode::SExt, &[value], to),
}
}
fn half_the_range(width: u32) -> i128 {
match width {
32 => 0x5F00_0000,
_ => 0x43E0_0000_0000_0000,
}
}
pub fn bytes(func: &mut Func) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
if func[inst].opcode == Opcode::Bswap {
swap(func, inst);
}
}
}
fn swap(func: &mut Func, inst: Inst) {
let ty = produced(func, inst);
let Some(&arg) = func[func[inst].args].first() else { return };
if !ty.is_int() || !ty.is_scalar() || ty.bits() < 16 || ty.bits() % 8 != 0 {
return;
}
let mut value = arg;
let mut group = ty.bits() / 2;
while group >= 8 {
let mask = alternating(ty.bits(), group);
let keep = ahead_const(func, inst, Imm::int(mask, ty), ty);
let count = ahead_const(func, inst, Imm::int(i128::from(group), ty), ty);
let low = ahead(func, inst, Opcode::And, &[value, keep], ty);
let up = ahead(func, inst, Opcode::Shl, &[low, count], ty);
let down = ahead(func, inst, Opcode::LShr, &[value, count], ty);
let high = ahead(func, inst, Opcode::And, &[down, keep], ty);
if group == 8 {
becomes(func, inst, Opcode::Or, &[up, high]);
return;
}
value = ahead(func, inst, Opcode::Or, &[up, high], ty);
group /= 2;
}
}
fn alternating(width: u32, group: u32) -> i128 {
every(width, group * 2, group)
}
fn every(width: u32, step: u32, run: u32) -> i128 {
let ones = (1i128 << run) - 1;
let mut mask = 0i128;
let mut at = 0;
while at < width {
mask |= ones << at;
at += step;
}
mask
}
pub fn counts(func: &mut Func) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
match func[inst].opcode {
Opcode::Ctlz => searched(func, inst, true),
Opcode::Cttz => searched(func, inst, false),
_ => {}
}
}
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
if func[inst].opcode == Opcode::Ctpop {
counted(func, inst);
}
}
}
fn searched(func: &mut Func, inst: Inst, leading: bool) {
let ty = produced(func, inst);
let Some(&arg) = func[func[inst].args].first() else { return };
if !countable(ty) {
return;
}
let ones = ahead_const(func, inst, Imm::int(-1, ty), ty);
if leading {
let mut value = arg;
let mut by = 1;
while by < ty.bits() {
let count = ahead_const(func, inst, Imm::int(i128::from(by), ty), ty);
let down = ahead(func, inst, Opcode::LShr, &[value, count], ty);
value = ahead(func, inst, Opcode::Or, &[value, down], ty);
by *= 2;
}
let above = ahead(func, inst, Opcode::Xor, &[value, ones], ty);
becomes(func, inst, Opcode::Ctpop, &[above]);
return;
}
let missing = ahead(func, inst, Opcode::Xor, &[arg, ones], ty);
let less = ahead(func, inst, Opcode::Add, &[arg, ones], ty);
let below = ahead(func, inst, Opcode::And, &[missing, less], ty);
becomes(func, inst, Opcode::Ctpop, &[below]);
}
fn counted(func: &mut Func, inst: Inst) {
let ty = produced(func, inst);
let Some(&arg) = func[func[inst].args].first() else { return };
if !countable(ty) {
return;
}
let width = ty.bits();
let pairs = ahead_const(func, inst, Imm::int(alternating(width, 1), ty), ty);
let two = ahead_const(func, inst, Imm::int(2, ty), ty);
let one = ahead_const(func, inst, Imm::int(1, ty), ty);
let high = ahead(func, inst, Opcode::LShr, &[arg, one], ty);
let odd = ahead(func, inst, Opcode::And, &[high, pairs], ty);
let bits = ahead(func, inst, Opcode::Sub, &[arg, odd], ty);
let quads = ahead_const(func, inst, Imm::int(alternating(width, 2), ty), ty);
let low = ahead(func, inst, Opcode::And, &[bits, quads], ty);
let up = ahead(func, inst, Opcode::LShr, &[bits, two], ty);
let rest = ahead(func, inst, Opcode::And, &[up, quads], ty);
let nibbles = ahead(func, inst, Opcode::Add, &[low, rest], ty);
let four = ahead_const(func, inst, Imm::int(4, ty), ty);
let bytes = ahead_const(func, inst, Imm::int(alternating(width, 4), ty), ty);
let folded = ahead(func, inst, Opcode::LShr, &[nibbles, four], ty);
let summed = ahead(func, inst, Opcode::Add, &[nibbles, folded], ty);
if width == 8 {
becomes(func, inst, Opcode::And, &[summed, bytes]);
return;
}
let held = ahead(func, inst, Opcode::And, &[summed, bytes], ty);
let spread = ahead_const(func, inst, Imm::int(every(width, 8, 1), ty), ty);
let top = ahead_const(func, inst, Imm::int(i128::from(width - 8), ty), ty);
let total = ahead(func, inst, Opcode::Mul, &[held, spread], ty);
becomes(func, inst, Opcode::LShr, &[total, top]);
}
pub fn overflows(func: &mut Func) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
let mut forward = HashMap::new();
for inst in found {
let checked = match func[inst].opcode {
Opcode::UAddOverflow => Checked::Add(false),
Opcode::SAddOverflow => Checked::Add(true),
Opcode::USubOverflow => Checked::Sub(false),
Opcode::SSubOverflow => Checked::Sub(true),
Opcode::UMulOverflow => Checked::Mul(false),
Opcode::SMulOverflow => Checked::Mul(true),
_ => continue,
};
overflowed(func, inst, checked, &mut forward);
}
if !forward.is_empty() {
substitute(func, &forward);
}
}
#[derive(Debug, Clone, Copy)]
enum Checked {
Add(bool),
Sub(bool),
Mul(bool),
}
fn overflowed(func: &mut Func, inst: Inst, checked: Checked, forward: &mut HashMap<Value, Value>) {
let ty = produced(func, inst);
let [a, b] = func[func[inst].args] else { return };
if !countable(ty) {
return;
}
let (value, bit) = match checked {
Checked::Add(signed) => {
let value = ahead(func, inst, Opcode::Add, &[a, b], ty);
let bit = if signed {
let left = ahead(func, inst, Opcode::Xor, &[a, value], ty);
let right = ahead(func, inst, Opcode::Xor, &[b, value], ty);
let both = ahead(func, inst, Opcode::And, &[left, right], ty);
negative(func, inst, both, ty)
} else {
compared(func, inst, IntPred::Ult, value, a)
};
(value, bit)
}
Checked::Sub(signed) => {
let value = ahead(func, inst, Opcode::Sub, &[a, b], ty);
let bit = if signed {
let apart = ahead(func, inst, Opcode::Xor, &[a, b], ty);
let moved = ahead(func, inst, Opcode::Xor, &[a, value], ty);
let both = ahead(func, inst, Opcode::And, &[apart, moved], ty);
negative(func, inst, both, ty)
} else {
compared(func, inst, IntPred::Ult, a, b)
};
(value, bit)
}
Checked::Mul(signed) => {
let value = ahead(func, inst, Opcode::Mul, &[a, b], ty);
let high = high_half(func, inst, a, b, signed, ty);
let bit = if signed {
let sign = ahead_const(func, inst, Imm::int(i128::from(ty.bits() - 1), ty), ty);
let wanted = ahead(func, inst, Opcode::AShr, &[value, sign], ty);
compared(func, inst, IntPred::Ne, high, wanted)
} else {
let zero = ahead_const(func, inst, Imm::int(0, ty), ty);
compared(func, inst, IntPred::Ne, high, zero)
};
(value, bit)
}
};
let mut answers = func[inst].results();
if let (Some(wrapped), Some(flag)) = (answers.next(), answers.next()) {
forward.insert(wrapped, value);
forward.insert(flag, bit);
}
func.remove_inst(inst);
}
fn high_half(func: &mut Func, inst: Inst, a: Value, b: Value, signed: bool, ty: Type) -> Value {
let width = ty.bits();
let half = width / 2;
let shift = ahead_const(func, inst, Imm::int(i128::from(half), ty), ty);
let mask = ahead_const(func, inst, Imm::int((1i128 << half) - 1, ty), ty);
let al = ahead(func, inst, Opcode::And, &[a, mask], ty);
let ah = ahead(func, inst, Opcode::LShr, &[a, shift], ty);
let bl = ahead(func, inst, Opcode::And, &[b, mask], ty);
let bh = ahead(func, inst, Opcode::LShr, &[b, shift], ty);
let ll = ahead(func, inst, Opcode::Mul, &[al, bl], ty);
let lh = ahead(func, inst, Opcode::Mul, &[al, bh], ty);
let hl = ahead(func, inst, Opcode::Mul, &[ah, bl], ty);
let hh = ahead(func, inst, Opcode::Mul, &[ah, bh], ty);
let over = ahead(func, inst, Opcode::LShr, &[ll, shift], ty);
let lh_low = ahead(func, inst, Opcode::And, &[lh, mask], ty);
let hl_low = ahead(func, inst, Opcode::And, &[hl, mask], ty);
let some = ahead(func, inst, Opcode::Add, &[over, lh_low], ty);
let carry = ahead(func, inst, Opcode::Add, &[some, hl_low], ty);
let lh_high = ahead(func, inst, Opcode::LShr, &[lh, shift], ty);
let hl_high = ahead(func, inst, Opcode::LShr, &[hl, shift], ty);
let up = ahead(func, inst, Opcode::LShr, &[carry, shift], ty);
let first = ahead(func, inst, Opcode::Add, &[hh, lh_high], ty);
let second = ahead(func, inst, Opcode::Add, &[first, hl_high], ty);
let high = ahead(func, inst, Opcode::Add, &[second, up], ty);
if !signed {
return high;
}
let top = ahead_const(func, inst, Imm::int(i128::from(width - 1), ty), ty);
let a_sign = ahead(func, inst, Opcode::AShr, &[a, top], ty);
let b_sign = ahead(func, inst, Opcode::AShr, &[b, top], ty);
let a_owes = ahead(func, inst, Opcode::And, &[a_sign, b], ty);
let b_owes = ahead(func, inst, Opcode::And, &[b_sign, a], ty);
let once = ahead(func, inst, Opcode::Sub, &[high, a_owes], ty);
ahead(func, inst, Opcode::Sub, &[once, b_owes], ty)
}
fn negative(func: &mut Func, inst: Inst, value: Value, ty: Type) -> Value {
let zero = ahead_const(func, inst, Imm::int(0, ty), ty);
compared(func, inst, IntPred::Slt, value, zero)
}
fn compared(func: &mut Func, inst: Inst, pred: IntPred, lhs: Value, rhs: Value) -> Value {
let ty = func[lhs].ty.with_lane(Type::I1);
let args = func.push_values(&[lhs, rhs]);
let extra = Extra::IntPred(pred);
written(func, inst, InstData { args, extra, ..InstData::new(Opcode::ICmp) }, ty)
}
fn substitute(func: &mut Func, forward: &HashMap<Value, Value>) {
let with = |value: Value| forward.get(&value).copied().unwrap_or(value);
for block in func.blocks().collect::<Vec<_>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
let args = func[inst].args;
func.rewrite(args, with);
for call in func.successors(inst).collect::<Vec<_>>() {
func.rewrite(call.args, with);
}
}
}
}
fn countable(ty: Type) -> bool {
ty.is_int()
&& ty.is_scalar()
&& ty.bits() >= 8
&& ty.bits() <= 64
&& ty.bits().is_power_of_two()
}
pub const UNROLL: usize = 32;
pub fn bulk(func: &mut Func, names: &mut Interner, word: u32) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
match func[inst].opcode {
Opcode::Memcpy => copy(func, names, inst, word),
Opcode::Memset => fill(func, names, inst, word),
Opcode::Memmove => library(func, names, inst, "memmove", word),
_ => {}
}
}
}
fn copy(func: &mut Func, names: &mut Interner, inst: Inst, word: u32) {
let [into, from] = func[func[inst].args] else { return };
let Extra::Mem(mem) = func[inst].extra else { return };
let info = func[mem];
let Some(plan) = chunks(info, word) else { return library(func, names, inst, "memcpy", word) };
for (at, width) in plan {
let ty = Type::int(width * 8);
let access = MemInfo { size: u64::from(width), align: width.min(info.align), ..info };
let there = stepped(func, inst, from, at);
let word = read(func, inst, there, access, ty);
let here = stepped(func, inst, into, at);
write(func, inst, word, here, access);
}
func.remove_inst(inst);
}
fn fill(func: &mut Func, names: &mut Interner, inst: Inst, word: u32) {
let [into, byte] = func[func[inst].args] else { return };
let Extra::Mem(mem) = func[inst].extra else { return };
let info = func[mem];
let Some(spelled) = literal(func, byte) else {
return library(func, names, inst, "memset", word);
};
let Some(plan) = chunks(info, word) else { return library(func, names, inst, "memset", word) };
for (at, width) in plan {
let ty = Type::int(width * 8);
let access = MemInfo { size: u64::from(width), align: width.min(info.align), ..info };
let value = ahead_const(func, inst, Imm::int(spread(spelled, width) as i128, ty), ty);
let here = stepped(func, inst, into, at);
write(func, inst, value, here, access);
}
func.remove_inst(inst);
}
fn library(func: &mut Func, names: &mut Interner, inst: Inst, routine: &str, word: u32) {
let [into, second] = func[func[inst].args] else { return };
let Extra::Mem(mem) = func[inst].extra else { return };
let size = func[mem].size;
let words = Type::int(word * 8);
let count = ahead_const(func, inst, Imm::int(i128::from(size), words), words);
let second = match routine {
"memset" => widened(func, inst, second),
_ => second,
};
let sig = func.add_signature(Signature::new().with_params(&[
Type::PTR,
if routine == "memset" { Type::int(32) } else { Type::PTR },
words,
]));
let callee = names.intern(routine);
let varargs = func.push_abis(&[]);
let info = func.add_call(CallInfo { callee: Some(callee), signature: sig, varargs });
let args = func.push_values(&[into, second, count]);
let data = &mut func[inst];
data.opcode = Opcode::Call;
data.args = args;
data.extra = Extra::Call(info);
data.flags = data.flags.intersection(Flags::legal_on(Opcode::Call));
}
fn widened(func: &mut Func, inst: Inst, value: Value) -> Value {
let int = Type::int(32);
let ty = func[value].ty;
if ty == int {
return value;
}
ahead(func, inst, Opcode::ZExt, &[value], int)
}
fn chunks(info: MemInfo, word: u32) -> Option<Vec<(u64, u32)>> {
plan(info.size, info.align, word)
}
pub(crate) fn plan(size: u64, align: u32, word: u32) -> Option<Vec<(u64, u32)>> {
let widest = word.min(align).max(1);
if !widest.is_power_of_two() {
return None;
}
let mut plan = Vec::new();
let mut at = 0;
let mut width = u64::from(widest);
while at < size {
while width > size - at {
width /= 2;
}
plan.push((at, u32::try_from(width).ok()?));
at += width;
if plan.len() > UNROLL {
return None;
}
}
Some(plan)
}
fn literal(func: &Func, value: Value) -> Option<u8> {
let Def::Result { inst, .. } = func[value].def else { return None };
if func[inst].opcode != Opcode::IConst {
return None;
}
let Extra::Imm(imm) = func[inst].extra else { return None };
u8::try_from(func[imm].bits() & 0xff).ok()
}
fn spread(byte: u8, width: u32) -> u64 {
(0..width).fold(0, |word, at| word | u64::from(byte) << (at * 8))
}
fn stepped(func: &mut Func, inst: Inst, block: Value, at: u64) -> Value {
if at == 0 {
return block;
}
let step = ahead_const(func, inst, Imm::int(i128::from(at), Type::int(64)), Type::int(64));
ahead(func, inst, Opcode::PtrAdd, &[block, step], Type::PTR)
}
fn read(func: &mut Func, inst: Inst, from: Value, info: MemInfo, ty: Type) -> Value {
let extra = Extra::Mem(func.add_mem(info));
let args = func.push_values(&[from]);
written(func, inst, InstData { args, extra, ..InstData::new(Opcode::Load) }, ty)
}
fn write(func: &mut Func, inst: Inst, value: Value, into: Value, info: MemInfo) {
let span = func.span(inst);
let extra = Extra::Mem(func.add_mem(info));
let args = func.push_values(&[value, into]);
let data = InstData { args, extra, ..InstData::new(Opcode::Store) };
let made = func.create_inst(data, &[], span);
func.insert_before(made, inst);
}
fn holder(bits: u32, signed: bool) -> Option<u32> {
match if signed { bits } else { bits + 1 } {
..=32 => Some(32),
33..=64 => Some(64),
_ => None,
}
}
fn produced(func: &Func, inst: Inst) -> Type {
func[inst].first_result.map_or(Type::VOID, |value| func[value].ty)
}
fn ahead(func: &mut Func, inst: Inst, opcode: Opcode, args: &[Value], ty: Type) -> Value {
let args = func.push_values(args);
written(func, inst, InstData { args, ..InstData::new(opcode) }, ty)
}
fn ahead_cmp(func: &mut Func, inst: Inst, opcode: Opcode, extra: Extra, args: &[Value]) -> Value {
let args = func.push_values(args);
written(func, inst, InstData { args, extra, ..InstData::new(opcode) }, Type::I1)
}
fn ahead_const(func: &mut Func, inst: Inst, imm: Imm, ty: Type) -> Value {
let extra = Extra::Imm(func.add_imm(imm));
written(func, inst, InstData { extra, ..InstData::new(Opcode::IConst) }, ty)
}
fn ahead_float(func: &mut Func, inst: Inst, bits: u128, ty: Type) -> Value {
let extra = Extra::Imm(func.add_imm(Imm::from_bits(bits)));
written(func, inst, InstData { extra, ..InstData::new(Opcode::FConst) }, ty)
}
fn written(func: &mut Func, inst: Inst, data: InstData, ty: Type) -> Value {
let span = func.span(inst);
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, inst);
func[made].first_result.expect("an instruction created with one result has one")
}
fn becomes(func: &mut Func, inst: Inst, opcode: Opcode, args: &[Value]) {
let args = func.push_values(args);
let data = &mut func[inst];
data.opcode = opcode;
data.args = args;
data.extra = Extra::None;
data.flags = data.flags.intersection(Flags::legal_on(opcode));
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, Flags, Float, Func, Module, Opcode, Signature, Type};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use rucc_ir::{Extra, InstData, MemInfo, MemOrder, Restrict};
use super::{
UNROLL, alternating, bulk, bytes, chunks, counts, every, floats, orderings, overflows,
spread,
};
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
fn printed(func: &Func, names: &mut Interner) -> String {
let module = Module::new(names.intern("sw.c"), &target());
rucc_ir::print_func(&module, func, names)
}
fn one(
params: &[Type],
returns: &[Type],
body: impl FnOnce(&mut Builder<'_>, &[rucc_ir::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 f64() -> Type {
Type::float(Float::F64)
}
fn f32() -> Type {
Type::float(Float::F32)
}
fn f80() -> Type {
Type::float(Float::F80)
}
const CASES: &[u64] = &[
0,
1,
2,
0x7FFF_FFFF,
0x8000_0000,
0xFFFF_FFFF,
0x0020_0000_0000_0000,
0x0020_0000_0000_0001,
0x7FFF_FFFF_FFFF_FFFF,
0x8000_0000_0000_0000,
0x8000_0000_0000_0001,
0x8000_0000_0000_0400,
0xFFFF_FFFF_FFFF_F800,
0xFFFF_FFFF_FFFF_FFFF,
];
fn valid(func: &Func, names: &mut Interner) {
let module = Module::new(names.intern("f.c"), &target());
rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
}
#[test]
fn a_float_constant_becomes_the_integer_that_spells_it_and_a_reading_of_those_bits() {
let (mut names, mut func) = one(&[], &[f64()], |build, _| {
let k = build.fconst(f64(), 0x3ff8_0000_0000_0000);
build.ret(&[k]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("fconst"), "the float constant is gone: {text}");
assert!(text.contains("iconst.i64 4609434218613702656"), "the bits, as an integer: {text}");
assert!(text.contains("bitcast"), "read back as the float: {text}");
}
#[test]
fn a_constant_at_the_narrow_format_is_an_integer_of_the_narrow_width() {
let (mut names, mut func) = one(&[], &[f32()], |build, _| {
let k = build.fconst(f32(), 0x4020_0000);
build.ret(&[k]);
});
floats(&mut func);
assert!(printed(&func, &mut names).contains("iconst.i32"), "an i32, not an i64");
}
#[test]
fn a_negation_flips_the_sign_bit_and_touches_no_other() {
let (mut names, mut func) = one(&[f64()], &[f64()], |build, args| {
let n = build.unary(Opcode::FNeg, args[0], f64());
build.ret(&[n]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("fneg"), "the negation is gone: {text}");
assert!(!text.contains("fsub"), "and it did not become a subtraction: {text}");
assert!(text.contains("iconst.i64 -9223372036854775808"), "the sign bit alone: {text}");
assert_eq!(text.matches("xor").count(), 1, "one exclusive or: {text}");
assert_eq!(text.matches("bitcast").count(), 2, "there and back: {text}");
}
#[test]
fn an_unsigned_integer_becoming_a_float_widens_first_and_then_converts_as_signed() {
let (mut names, mut func) = one(&[Type::int(32)], &[f64()], |build, args| {
let d = build.unary(Opcode::UIToFP, args[0], f64());
build.ret(&[d]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("uitofp"), "the unsigned conversion is gone: {text}");
assert!(text.contains("zext.i64"), "widened with zeroes: {text}");
assert!(text.contains("sitofp.f64"), "converted as signed: {text}");
}
#[test]
fn a_float_becoming_an_unsigned_integer_converts_as_signed_first_and_then_narrows() {
let (mut names, mut func) = one(&[f64()], &[Type::int(32)], |build, args| {
let n = build.unary(Opcode::FPToUI, args[0], Type::int(32));
build.ret(&[n]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("fptoui"), "the unsigned conversion is gone: {text}");
assert!(text.contains("fptosi.i64"), "converted as signed: {text}");
assert!(text.contains("trunc.i32"), "and narrowed to what was asked: {text}");
}
#[test]
fn a_conversion_narrower_than_the_machine_has_is_one_it_has_and_a_narrowing() {
let (mut names, mut func) = one(&[f64()], &[Type::int(8)], |build, args| {
let n = build.unary(Opcode::FPToSI, args[0], Type::int(8));
build.ret(&[n]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(text.contains("fptosi.i32"), "converted at a width there is one at: {text}");
assert!(text.contains("trunc.i8"), "and narrowed to what was asked: {text}");
}
#[test]
fn a_signed_integer_narrower_than_the_machine_converts_from_is_widened_with_its_sign() {
let (mut names, mut func) = one(&[Type::int(8)], &[f64()], |build, args| {
let d = build.unary(Opcode::SIToFP, args[0], f64());
build.ret(&[d]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(text.contains("sext.i32"), "widened with the sign and not with zeroes: {text}");
assert!(!text.contains("zext"), "widened with the sign and not with zeroes: {text}");
assert!(text.contains("sitofp.f64"), "converted at a width there is one at: {text}");
}
#[test]
fn the_width_a_conversion_happens_at_is_the_narrowest_one_that_holds_the_values() {
use super::holder;
for bits in [1, 8, 16, 32] {
assert_eq!(holder(bits, true), Some(32), "a signed {bits} bit value fits in an int");
}
assert_eq!(holder(64, true), Some(64));
for bits in [1, 8, 16, 31] {
assert_eq!(holder(bits, false), Some(32), "an unsigned {bits} bit value does too");
}
assert_eq!(holder(32, false), Some(64));
assert_eq!(holder(64, false), None);
}
#[test]
fn the_unsigned_conversions_at_the_widest_width_become_the_signed_one_and_a_correction() {
for float in [f32(), f64()] {
let (mut names, mut func) = one(&[Type::int(64)], &[float], |build, args| {
let d = build.unary(Opcode::UIToFP, args[0], float);
build.ret(&[d]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("uitofp"), "the unsigned conversion is gone: {text}");
assert!(text.contains("sitofp"), "the signed one is what is left: {text}");
assert!(text.contains("lshr"), "the value is halved: {text}");
assert!(text.contains("fadd"), "and doubled again afterwards: {text}");
valid(&func, &mut names);
}
for float in [f32(), f64()] {
let (mut names, mut func) = one(&[float], &[Type::int(64)], |build, args| {
let n = build.unary(Opcode::FPToUI, args[0], Type::int(64));
build.ret(&[n]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("fptoui"), "the unsigned conversion is gone: {text}");
assert!(text.contains("fptosi"), "the signed one is what is left: {text}");
assert!(text.contains("fsub"), "the value is brought down: {text}");
assert!(text.contains("shl"), "and the top bit goes back on: {text}");
valid(&func, &mut names);
}
}
#[test]
fn the_widest_unsigned_conversions_are_written_without_a_branch() {
let (_, mut func) = one(&[Type::int(64)], &[f64()], |build, args| {
let d = build.unary(Opcode::UIToFP, args[0], f64());
build.ret(&[d]);
});
floats(&mut func);
assert_eq!(func.blocks().count(), 1, "the conversion did not split the block");
let (_, mut func) = one(&[f64()], &[Type::int(64)], |build, args| {
let n = build.unary(Opcode::FPToUI, args[0], Type::int(64));
build.ret(&[n]);
});
floats(&mut func);
assert_eq!(func.blocks().count(), 1, "nor did the other one");
}
#[test]
fn the_arithmetic_the_widest_unsigned_conversions_do_is_the_conversion() {
for &x in CASES {
let mask = if (x as i64) < 0 { u64::MAX } else { 0 };
let odd = (x >> 1) | (x & 1);
let source = x ^ ((x ^ odd) & mask);
let converted = source as i64 as f64;
let addend = f64::from_bits(converted.to_bits() & mask);
assert_eq!(converted + addend, x as f64, "converting {x:#x} into a double");
}
for &x in CASES {
let d = x as f64;
if d >= 18_446_744_073_709_551_616.0 {
continue;
}
let half = f64::from_bits(0x43E0_0000_0000_0000);
let mask = if d >= half { u64::MAX } else { 0 };
let taken = f64::from_bits(half.to_bits() & mask);
let low = (d - taken) as i64;
let top = u64::from(d >= half) << 63;
assert_eq!(low as u64 ^ top, d as u64, "converting {d} into an unsigned word");
}
}
#[test]
fn the_unsigned_conversions_at_eighty_bits_correct_with_a_multiply_instead_of_a_mask() {
let (mut names, mut func) = one(&[Type::int(64)], &[f80()], |build, args| {
let d = build.unary(Opcode::UIToFP, args[0], f80());
build.ret(&[d]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("uitofp"), "the unsigned conversion is gone: {text}");
assert!(text.contains("sitofp.f80"), "the signed one is what is left: {text}");
assert!(!text.contains("bitcast"), "and nothing reads the float as an integer: {text}");
assert!(!text.contains("lshr"), "nor is the value halved, since nothing rounds: {text}");
assert!(text.contains("fmul "), "the constant is taken or not by a multiply: {text}");
assert!(text.contains("fadd "), "and added to what the conversion gave: {text}");
assert_eq!(func.blocks().count(), 1, "the conversion did not split the block");
valid(&func, &mut names);
let (mut names, mut func) = one(&[f80()], &[Type::int(64)], |build, args| {
let n = build.unary(Opcode::FPToUI, args[0], Type::int(64));
build.ret(&[n]);
});
floats(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("fptoui"), "the unsigned conversion is gone: {text}");
assert!(text.contains("fptosi.i64"), "the signed one is what is left: {text}");
assert!(!text.contains("bitcast"), "and nothing reads the float as an integer: {text}");
assert!(text.contains("fmul "), "the constant is taken or not by a multiply: {text}");
assert!(text.contains("fsub "), "and subtracted before the conversion: {text}");
assert!(text.contains("shl"), "with the top bit going back on after it: {text}");
assert_eq!(func.blocks().count(), 1, "nor did the other one");
valid(&func, &mut names);
}
#[test]
fn nothing_in_either_conversion_at_eighty_bits_rounds() {
fn exact(v: i128) -> bool {
let mag = v.unsigned_abs();
mag == 0 || (mag >> mag.trailing_zeros()) < 1 << 64
}
for &x in CASES {
let signed = i128::from(x as i64);
let addend = if (x as i64) < 0 { 1i128 << 64 } else { 0 };
assert!(exact(signed), "the conversion of {x:#x} read as signed is exact");
assert!(exact(addend), "and so is the constant it gets");
assert!(exact(signed + addend), "and so is the sum");
assert_eq!(signed + addend, i128::from(x), "converting {x:#x} into a long double");
}
for &x in CASES {
let value = i128::from(x);
let taken = if value >= 1 << 63 { 1i128 << 63 } else { 0 };
let under = value - taken;
assert!(exact(under), "the subtraction that brings {x:#x} into range is exact");
let top = u64::from(value >= 1 << 63) << 63;
assert_eq!(under as u64 ^ top, x, "converting {x:#x} back into an unsigned word");
}
}
#[test]
fn what_the_float_rewrites_leave_is_valid_ir() {
let (mut names, mut func) = one(&[Type::int(32)], &[f64()], |build, args| {
let k = build.fconst(f64(), 0x3ff8_0000_0000_0000);
let d = build.unary(Opcode::UIToFP, args[0], f64());
let n = build.unary(Opcode::FNeg, d, f64());
let s = build.binary(Opcode::FAdd, n, k, Flags::NONE);
build.ret(&[s]);
});
floats(&mut func);
let module = Module::new(names.intern("f.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the rewrite builds valid IR");
}
#[test]
fn a_function_with_no_floats_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
floats(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
fn access(size: u64, align: u32) -> MemInfo {
MemInfo { size, align, order: MemOrder::NotAtomic, tbaa: None, restrict: Restrict::NONE }
}
fn moving(opcode: Opcode, size: u64, align: u32, byte: Option<i128>) -> (Interner, Func) {
one(&[Type::PTR, Type::PTR], &[], |build, args| {
let second = match byte {
Some(value) => build.iconst(Type::int(8), value),
None => args[1],
};
let mem = build.func().add_mem(access(size, align));
let operands = build.func().push_values(&[args[0], second]);
let data = InstData { args: operands, extra: Extra::Mem(mem), ..InstData::new(opcode) };
build.inst(data, &[]);
build.ret(&[]);
})
}
fn copying(size: u64, align: u32) -> (Interner, Func) {
moving(Opcode::Memcpy, size, align, None)
}
fn filling(size: u64, align: u32, byte: i128) -> (Interner, Func) {
moving(Opcode::Memset, size, align, Some(byte))
}
fn widths(size: u64, align: u32) -> Option<Vec<u32>> {
Some(chunks(access(size, align), 8)?.into_iter().map(|(_, width)| width).collect())
}
#[test]
fn a_copy_becomes_a_load_and_a_store_for_each_word_of_it() {
let (mut names, mut func) = copying(16, 8);
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(!text.contains("memcpy"), "the copy is gone: {text}");
assert_eq!(text.matches("load.i64").count(), 2, "a load per word: {text}");
assert_eq!(text.matches("store").count(), 2, "a store per word: {text}");
assert_eq!(
text.matches("ptr_add").count(),
2,
"no offset for the word at the front: {text}"
);
}
#[test]
fn a_word_is_as_wide_as_the_block_is_aligned_to() {
assert_eq!(widths(16, 8), Some(vec![8, 8]));
assert_eq!(widths(16, 4), Some(vec![4, 4, 4, 4]));
assert_eq!(widths(4, 1), Some(vec![1, 1, 1, 1]));
}
#[test]
fn what_is_left_over_is_narrower_words_and_not_a_run_of_bytes() {
assert_eq!(widths(13, 8), Some(vec![8, 4, 1]));
assert_eq!(widths(3, 8), Some(vec![2, 1]));
assert_eq!(widths(1, 8), Some(vec![1]));
}
#[test]
fn every_word_starts_somewhere_it_is_aligned_for() {
for (at, width) in chunks(access(13, 8), 8).expect("a plan for thirteen bytes") {
assert_eq!(at % u64::from(width), 0, "{at} is a multiple of {width}");
}
}
#[test]
fn a_fill_is_the_byte_spread_across_each_word() {
let (mut names, mut func) = filling(16, 8, 0);
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(!text.contains("memset"), "the fill is gone: {text}");
assert_eq!(text.matches("store").count(), 2, "a store per word: {text}");
assert!(!text.contains("load"), "a fill reads nothing: {text}");
}
#[test]
fn the_byte_is_repeated_across_the_word_it_is_stored_as() {
assert_eq!(spread(0, 8), 0);
assert_eq!(spread(0xff, 1), 0xff);
assert_eq!(spread(0xff, 4), 0xffff_ffff);
assert_eq!(spread(0xab, 2), 0xabab);
assert_eq!(spread(0xab, 8), 0xabab_abab_abab_abab);
}
#[test]
fn a_copy_too_large_to_unroll_becomes_a_call_to_the_runtime() {
let size = u64::try_from(UNROLL).expect("a small threshold") + 1;
let (mut names, mut func) = copying(size, 1);
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(text.contains("call @memcpy"), "a call and not a bulk move: {text}");
let (mut names, mut func) = copying(size - 1, 1);
bulk(&mut func, &mut names, 8);
assert!(!printed(&func, &mut names).contains("memcpy"), "one word under it is unrolled");
}
#[test]
fn the_call_passes_the_size_that_the_instruction_carried_beside_it() {
let size = u64::try_from(UNROLL).expect("a small threshold") + 1;
let (mut names, mut func) = copying(size, 1);
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(text.contains(&format!("{size}")), "the size is an argument now: {text}");
}
#[test]
fn a_move_is_a_call_however_small_it_is() {
let (mut names, mut func) = moving(Opcode::Memmove, 8, 8, None);
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(text.contains("call @memmove"), "a call and not a run of moves: {text}");
}
#[test]
fn a_fill_whose_byte_is_not_a_constant_becomes_a_call() {
let (mut names, mut func) = one(&[Type::PTR, Type::int(8)], &[], |build, args| {
let mem = build.func().add_mem(access(8, 8));
let operands = build.func().push_values(&[args[0], args[1]]);
let data = InstData {
args: operands,
extra: Extra::Mem(mem),
..InstData::new(Opcode::Memset)
};
build.inst(data, &[]);
build.ret(&[]);
});
bulk(&mut func, &mut names, 8);
let text = printed(&func, &mut names);
assert!(text.contains("call @memset"), "a call and not a run of stores: {text}");
assert!(text.contains("zext.i32"), "the byte is widened to what C passes: {text}");
}
#[test]
fn no_word_is_wider_than_the_machine_moves_at_once() {
assert_eq!(chunks(access(8, 8), 4).map(|plan| plan.len()), Some(2));
assert_eq!(chunks(access(8, 8), 8).map(|plan| plan.len()), Some(1));
}
#[test]
fn what_a_copy_becomes_is_ir_that_verifies() {
let (mut names, mut func) = copying(13, 8);
bulk(&mut func, &mut names, 8);
let module = Module::new(names.intern("c.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the rewrite builds valid IR");
}
#[test]
fn what_a_fill_becomes_is_ir_that_verifies() {
let (mut names, mut func) = filling(13, 8, 0xff);
bulk(&mut func, &mut names, 8);
let module = Module::new(names.intern("f.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the rewrite builds valid IR");
}
#[test]
fn what_a_copy_too_large_to_unroll_becomes_is_ir_that_verifies() {
let size = u64::try_from(UNROLL).expect("a small threshold") + 1;
let (mut names, mut func) = copying(size, 1);
bulk(&mut func, &mut names, 8);
let module = Module::new(names.intern("c.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the call is valid IR");
}
#[test]
fn a_function_with_no_bulk_move_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
bulk(&mut func, &mut names, 8);
assert_eq!(printed(&func, &mut names), before);
}
fn swapping(width: u32) -> (Interner, Func) {
let ty = Type::int(width);
one(&[ty], &[ty], |build, args| {
let s = build.unary(Opcode::Bswap, args[0], ty);
build.ret(&[s]);
})
}
#[test]
fn the_masks_are_the_alternating_runs_of_the_group_being_swapped() {
assert_eq!(alternating(32, 16), 0x0000_ffff);
assert_eq!(alternating(32, 8), 0x00ff_00ff);
assert_eq!(alternating(16, 8), 0x00ff);
assert_eq!(alternating(64, 32), 0x0000_0000_ffff_ffff);
assert_eq!(alternating(64, 16), 0x0000_ffff_0000_ffff);
assert_eq!(alternating(64, 8), 0x00ff_00ff_00ff_00ff);
}
#[test]
fn a_two_byte_swap_is_one_exchange_of_neighbouring_bytes() {
let (mut names, mut func) = swapping(16);
bytes(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("bswap"), "the instruction is gone: {text}");
assert!(text.contains("iconst.i16 255"), "the low byte of the pair: {text}");
assert_eq!(text.matches("shl").count(), 1, "one shift up: {text}");
assert_eq!(text.matches("lshr").count(), 1, "one shift down: {text}");
assert_eq!(text.matches(" or ").count(), 1, "and the two put together: {text}");
}
#[test]
fn a_wider_swap_is_the_same_exchange_once_per_halving() {
for (width, steps) in [(16u32, 1usize), (32, 2), (64, 3)] {
let (mut names, mut func) = swapping(width);
bytes(&mut func);
let text = printed(&func, &mut names);
assert_eq!(text.matches("shl").count(), steps, "at {width}: {text}");
assert_eq!(text.matches("lshr").count(), steps, "at {width}: {text}");
assert_eq!(text.matches(" and ").count(), steps * 2, "at {width}: {text}");
assert_eq!(text.matches(" or ").count(), steps, "at {width}: {text}");
}
}
#[test]
fn the_shift_counts_are_the_group_width_halving_as_it_goes() {
let (mut names, mut func) = swapping(64);
bytes(&mut func);
let text = printed(&func, &mut names);
for count in ["iconst.i64 32", "iconst.i64 16", "iconst.i64 8"] {
assert!(text.contains(count), "{count} is a step: {text}");
}
}
#[test]
fn what_a_byte_swap_becomes_is_ir_that_verifies() {
let (mut names, mut func) = swapping(32);
bytes(&mut func);
let module = Module::new(names.intern("b.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the rewrite builds valid IR");
}
#[test]
fn a_function_with_no_byte_swap_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
bytes(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
fn counting(op: Opcode, width: u32) -> (Interner, Func) {
let ty = Type::int(width);
one(&[ty], &[ty], |build, args| {
let c = build.unary(op, args[0], ty);
build.ret(&[c]);
})
}
#[test]
fn the_counting_masks_are_the_ones_the_halving_sum_is_written_with() {
assert_eq!(alternating(32, 1), 0x5555_5555);
assert_eq!(alternating(32, 2), 0x3333_3333);
assert_eq!(alternating(32, 4), 0x0f0f_0f0f);
assert_eq!(every(32, 8, 1), 0x0101_0101);
assert_eq!(every(64, 8, 1), 0x0101_0101_0101_0101);
}
#[test]
fn a_set_bit_count_is_the_halving_sum_and_a_multiply_that_adds_the_bytes() {
let (mut names, mut func) = counting(Opcode::Ctpop, 32);
counts(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("ctpop"), "the instruction is gone: {text}");
assert!(text.contains("iconst.i32 1431655765"), "the pairs mask: {text}");
assert!(text.contains("iconst.i32 858993459"), "the nibbles mask: {text}");
assert!(text.contains("iconst.i32 252645135"), "the bytes mask: {text}");
assert_eq!(text.matches(" mul ").count(), 1, "one multiply: {text}");
assert!(text.contains("iconst.i32 24"), "and the top byte is the answer: {text}");
}
#[test]
fn a_count_of_one_byte_stops_before_the_multiply() {
let (mut names, mut func) = counting(Opcode::Ctpop, 8);
counts(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("ctpop"), "{text}");
assert!(!text.contains(" mul "), "nothing to add together: {text}");
}
#[test]
fn a_leading_zero_count_smears_the_value_down_and_counts_the_complement() {
let (mut names, mut func) = counting(Opcode::Ctlz, 32);
counts(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("ctlz"), "the instruction is gone: {text}");
assert!(!text.contains("ctpop"), "and so is the count it became: {text}");
for by in ["iconst.i32 1", "iconst.i32 2", "iconst.i32 4", "iconst.i32 8", "iconst.i32 16"]
{
assert!(text.contains(by), "{by} is a smearing step: {text}");
}
assert_eq!(text.matches(" xor ").count(), 1, "one complement: {text}");
}
#[test]
fn a_trailing_zero_count_masks_the_bits_below_the_lowest_set_one() {
let (mut names, mut func) = counting(Opcode::Cttz, 32);
counts(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("cttz"), "the instruction is gone: {text}");
assert!(!text.contains("ctpop"), "and so is the count it became: {text}");
assert!(text.contains("iconst.i32 -1"), "the complement and the decrement: {text}");
assert_eq!(text.matches(" xor ").count(), 1, "one complement: {text}");
assert!(text.matches(" or ").count() <= 1, "no smearing run: {text}");
}
#[test]
fn what_a_bit_count_becomes_is_ir_that_verifies() {
for op in [Opcode::Ctpop, Opcode::Ctlz, Opcode::Cttz] {
for width in [8u32, 16, 32, 64] {
let (mut names, mut func) = counting(op, width);
counts(&mut func);
let module = Module::new(names.intern("c.c"), &target());
rucc_ir::verify_func(&module, &func, &names)
.unwrap_or_else(|e| panic!("{op:?} at {width}: {e:?}"));
}
}
}
#[test]
fn a_width_the_halving_sum_is_not_written_for_is_left_alone() {
let (mut names, mut func) = counting(Opcode::Ctpop, 24);
counts(&mut func);
assert!(printed(&func, &mut names).contains("ctpop"), "left as it was");
}
#[test]
fn a_function_with_no_bit_count_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
counts(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
fn checking(op: Opcode, width: u32) -> (Interner, Func) {
let ty = Type::int(width);
let bit = ty.with_lane(Type::I1);
one(&[ty, ty], &[ty, bit], |build, args| {
let (value, flag) = build.checked(op, args[0], args[1]);
build.ret(&[value, flag]);
})
}
#[test]
fn a_checked_unsigned_add_becomes_an_add_and_one_comparison() {
let (mut names, mut func) = checking(Opcode::UAddOverflow, 32);
overflows(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("uadd_overflow"), "the instruction is gone: {text}");
assert_eq!(text.matches(" add ").count(), 1, "one add: {text}");
assert_eq!(text.matches("icmp ult").count(), 1, "and one comparison: {text}");
assert!(!text.contains(" xor "), "nothing about sign bits: {text}");
}
#[test]
fn a_checked_signed_add_becomes_an_add_and_the_sign_bit_of_two_exclusive_ors() {
let (mut names, mut func) = checking(Opcode::SAddOverflow, 32);
overflows(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("sadd_overflow"), "the instruction is gone: {text}");
assert_eq!(text.matches(" add ").count(), 1, "one add: {text}");
assert_eq!(text.matches(" xor ").count(), 2, "the answer against each operand: {text}");
assert_eq!(text.matches(" and ").count(), 1, "both at once: {text}");
assert!(text.contains("icmp slt"), "and its sign bit: {text}");
}
#[test]
fn a_checked_unsigned_subtract_compares_the_operands_and_not_the_answer() {
let (mut names, mut func) = checking(Opcode::USubOverflow, 64);
overflows(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("usub_overflow"), "the instruction is gone: {text}");
assert_eq!(text.matches(" sub ").count(), 1, "one subtract: {text}");
assert!(text.contains("icmp ult %0, %1"), "the operands, in order: {text}");
}
#[test]
fn a_checked_multiply_becomes_a_multiply_and_the_high_half_of_the_product() {
let (mut names, mut func) = checking(Opcode::UMulOverflow, 64);
overflows(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("umul_overflow"), "the instruction is gone: {text}");
assert_eq!(text.matches(" mul ").count(), 5, "the answer and the four halves: {text}");
assert!(text.contains("iconst.i64 32"), "split at half the width: {text}");
assert!(text.contains("iconst.i64 4294967295"), "and masked to it: {text}");
assert!(text.contains("icmp ne"), "the high half against zero: {text}");
assert!(!text.contains("ashr"), "and nothing corrected for sign: {text}");
}
#[test]
fn a_checked_signed_multiply_corrects_the_high_half_for_each_negative_operand() {
let (mut names, mut func) = checking(Opcode::SMulOverflow, 64);
overflows(&mut func);
let text = printed(&func, &mut names);
assert!(!text.contains("smul_overflow"), "the instruction is gone: {text}");
assert_eq!(
text.matches(" ashr ").count(),
3,
"each operand's sign, and the answer: {text}"
);
assert!(text.contains("iconst.i64 63"), "spread from the top bit: {text}");
assert_eq!(text.matches(" sub ").count(), 2, "one correction per operand: {text}");
}
#[test]
fn both_results_are_substituted_into_whoever_was_reading_them() {
let (mut names, mut func) = checking(Opcode::SAddOverflow, 32);
overflows(&mut func);
let text = printed(&func, &mut names);
assert_eq!(
text,
concat!(
"func @f(i32, i32) -> (i32, i1), linkage(external) {\n",
"block0(%0: i32, %1: i32):\n",
" %2 = add %0, %1\n",
" %3 = xor %0, %2\n",
" %4 = xor %1, %2\n",
" %5 = and %3, %4\n",
" %6 = iconst.i32 0\n",
" %7 = icmp slt %5, %6\n",
" return %2, %7\n",
"}\n",
),
);
}
#[test]
fn what_an_overflow_check_becomes_is_ir_that_verifies() {
let all = [
Opcode::UAddOverflow,
Opcode::SAddOverflow,
Opcode::USubOverflow,
Opcode::SSubOverflow,
Opcode::UMulOverflow,
Opcode::SMulOverflow,
];
for op in all {
for width in [8u32, 16, 32, 64] {
let (mut names, mut func) = checking(op, width);
overflows(&mut func);
let module = Module::new(names.intern("c.c"), &target());
rucc_ir::verify_func(&module, &func, &names)
.unwrap_or_else(|e| panic!("{op:?} at {width}: {e:?}"));
}
}
}
#[test]
fn a_width_the_split_is_not_written_for_is_left_alone() {
let (mut names, mut func) = checking(Opcode::UMulOverflow, 24);
overflows(&mut func);
assert!(printed(&func, &mut names).contains("umul_overflow"), "left as it was");
}
#[test]
fn a_function_with_no_overflow_check_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
overflows(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
fn reading(ty: Type, align: u32, order: MemOrder) -> (Interner, Func) {
one(&[Type::PTR], &[ty], |build, args| {
let info = MemInfo { order, ..access(0, align) };
let value = build.atomic_load(ty, args[0], info, Flags::NONE);
build.ret(&[value]);
})
}
fn writing(ty: Type, align: u32, order: MemOrder) -> (Interner, Func) {
one(&[Type::PTR, ty], &[], |build, args| {
let info = MemInfo { order, ..access(0, align) };
build.atomic_store(args[1], args[0], info, Flags::NONE);
build.ret(&[]);
})
}
#[test]
fn an_ordered_access_becomes_the_plain_one_this_machine_already_orders() {
for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
let (mut names, mut func) = reading(Type::int(32), 4, order);
orderings(&mut func, 8);
let text = printed(&func, &mut names);
assert!(text.contains("load.i32"), "{order:?}: {text}");
assert!(!text.contains("atomic_load"), "{order:?}: {text}");
assert!(!text.contains(order.name()), "the ordering came off: {text}");
}
for order in [MemOrder::Relaxed, MemOrder::Release] {
let (mut names, mut func) = writing(Type::int(32), 4, order);
orderings(&mut func, 8);
let text = printed(&func, &mut names);
assert!(text.contains("store %1 -> %0"), "{order:?}: {text}");
assert!(!text.contains("atomic_store"), "{order:?}: {text}");
assert!(!text.contains("fence"), "{order:?} costs nothing here: {text}");
}
}
#[test]
fn the_strongest_store_keeps_a_barrier_behind_it() {
let (mut names, mut func) = writing(Type::int(32), 4, MemOrder::SeqCst);
orderings(&mut func, 8);
let text = printed(&func, &mut names);
let (before, after) = text.split_once("fence seq_cst").expect("a barrier");
assert!(before.contains("store %1 -> %0"), "the store comes first: {text}");
assert!(!after.contains("store"), "and nothing is between them: {text}");
assert!(!text.contains("atomic_store"), "{text}");
}
#[test]
fn a_barrier_is_left_for_the_place_that_knows_what_one_costs() {
for order in MemOrder::all().filter(|&order| order != MemOrder::NotAtomic) {
let (mut names, mut func) = one(&[], &[], |build, _| {
build.fence(order);
build.ret(&[]);
});
let before = printed(&func, &mut names);
orderings(&mut func, 8);
assert_eq!(printed(&func, &mut names), before, "{order:?}");
}
}
#[test]
fn an_access_this_machine_cannot_do_in_one_go_is_left_alone() {
for (ty, align) in [(Type::int(128), 16), (Type::int(64), 4)] {
let (mut names, mut func) = reading(ty, align, MemOrder::SeqCst);
orderings(&mut func, 8);
assert!(printed(&func, &mut names).contains("atomic_load"), "left as it was");
}
}
#[test]
fn what_the_ordered_accesses_become_verifies() {
for order in MemOrder::all().filter(|&order| order != MemOrder::NotAtomic) {
for (mut names, mut func) in
[reading(Type::int(32), 4, order), writing(Type::int(32), 4, order)]
{
if !order.is_valid_for_load() && !order.is_valid_for_store() {
continue;
}
orderings(&mut func, 8);
let module = Module::new(names.intern("a.c"), &target());
rucc_ir::verify_func(&module, &func, &names)
.unwrap_or_else(|e| panic!("{order:?}: {e:?}"));
}
}
}
#[test]
fn a_function_with_no_ordered_access_in_it_is_left_exactly_as_it_was() {
let (mut names, mut func) = one(&[Type::int(32)], &[Type::int(32)], |build, args| {
build.ret(&[args[0]]);
});
let before = printed(&func, &mut names);
orderings(&mut func, 8);
assert_eq!(printed(&func, &mut names), before);
}
}