use rucc_base::Interner;
use rucc_ir::{
BlockCall, Builder, CallInfo, Def, Extra, Flags, Func, Imm, Inst, InstData, IntPred, MemInfo,
Opcode, Signature, Type, Value,
};
pub fn switches(func: &mut Func) {
let found: Vec<Inst> = func
.blocks()
.filter_map(|block| func.terminator(block))
.filter(|&inst| func[inst].opcode == Opcode::Switch)
.collect();
for inst in found {
chain(func, inst);
}
}
fn chain(func: &mut Func, inst: Inst) {
let block = func.block_of(inst).expect("a terminator is in a block");
let span = func.span(inst);
let Extra::Switch(info) = func[inst].extra else { return };
let info = func[info];
let value = func[func[inst].args][0];
let ty = func[value].ty.lane();
let calls: Vec<BlockCall> = func[info.targets].to_vec();
let cases: Vec<Imm> = func[info.cases].to_vec();
let Some((default, arms)) = calls.split_first() else { return };
func.remove_inst(inst);
let Some((first, rest)) = arms.split_first() else {
let args: Vec<Value> = func[default.args].to_vec();
Builder::new(func, block).at(span).jump(default.block, &args);
return;
};
let mut at = block;
for (index, arm) in std::iter::once(first).chain(rest).enumerate() {
let last = index + 1 == arms.len();
let next = if last { default.block } else { func.create_block() };
let onward: Vec<Value> = if last { func[default.args].to_vec() } else { Vec::new() };
let taken: Vec<Value> = func[arm.args].to_vec();
let case = cases[index].signed(ty);
let mut build = Builder::new(func, at).at(span);
let want = build.iconst(ty, case);
let same = build.icmp(IntPred::Eq, value, want);
build.br_if(same, arm.block, &taken, next, &onward);
at = next;
}
}
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() {
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() {
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 { 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 { return };
if width == ty.bits() {
return;
}
let wide = ahead(func, inst, Opcode::FPToSI, &[arg], Type::int(width));
becomes(func, inst, Opcode::Trunc, &[wide]);
}
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_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 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));
}
#[must_use]
pub fn blocks_for(cases: usize) -> usize {
cases.saturating_sub(1)
}
#[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};
use super::{UNROLL, blocks_for, bulk, chunks, floats, spread, switches};
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
fn built(cases: &[i128]) -> (Interner, Func) {
let mut names = Interner::new();
let int = Type::int(32);
let mut func = Func::new(
names.intern("sw"),
Signature::new().with_params(&[int]).with_returns(&[int]),
);
let entry = func.create_block();
let x = func.append_param(entry, int);
let default = func.create_block();
let arms: Vec<_> = cases.iter().map(|_| func.create_block()).collect();
let table: Vec<(i128, rucc_ir::Block)> =
cases.iter().copied().zip(arms.iter().copied()).collect();
Builder::new(&mut func, entry).switch(x, default, &table);
for (index, &arm) in arms.iter().enumerate() {
let mut build = Builder::new(&mut func, arm);
let what = i128::try_from(index).expect("a small number of cases");
let v = build.iconst(int, (what + 1) * 10);
build.ret(&[v]);
}
let mut build = Builder::new(&mut func, default);
let v = build.iconst(int, 30);
build.ret(&[v]);
(names, func)
}
fn count(func: &Func) -> usize {
func.blocks().count()
}
fn printed(func: &Func, names: &mut Interner) -> String {
let module = Module::new(names.intern("sw.c"), &target());
rucc_ir::print_func(&module, func, names)
}
#[test]
fn a_switch_becomes_a_compare_and_a_branch_for_each_case() {
let (mut names, mut func) = built(&[1, 2]);
let before = count(&func);
switches(&mut func);
assert_eq!(count(&func), before + blocks_for(2));
let text = printed(&func, &mut names);
assert!(!text.contains("switch"), "the switch is gone: {text}");
assert_eq!(text.matches("icmp eq").count(), 2, "one compare per case: {text}");
assert_eq!(text.matches("br_if").count(), 2, "one branch per case: {text}");
}
#[test]
fn the_last_case_falls_to_the_default_rather_than_to_a_block_of_its_own() {
let (_, mut func) = built(&[7]);
let before = count(&func);
switches(&mut func);
assert_eq!(count(&func), before);
assert_eq!(blocks_for(1), 0);
}
#[test]
fn a_switch_with_only_a_default_is_a_jump() {
let (_, mut func) = built(&[]);
switches(&mut func);
let entry = func.entry().expect("an entry block");
let term = func.terminator(entry).expect("a terminator");
assert_eq!(func[term].opcode, Opcode::Jump);
}
#[test]
fn what_comes_out_is_valid_ir() {
let (mut names, mut func) = built(&[1, 2, 3, 4]);
switches(&mut func);
let module = Module::new(names.intern("sw.c"), &target());
rucc_ir::verify_func(&module, &func, &names).expect("the rewrite builds valid IR");
}
#[test]
fn a_function_with_no_switch_is_left_exactly_as_it_was() {
let mut names = Interner::new();
let int = Type::int(32);
let mut func =
Func::new(names.intern("f"), Signature::new().with_params(&[int]).with_returns(&[int]));
let entry = func.create_block();
let x = func.append_param(entry, int);
Builder::new(&mut func, entry).ret(&[x]);
let before = printed(&func, &mut names);
switches(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
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)
}
#[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_are_left_alone() {
let (mut names, mut func) = one(&[Type::int(64)], &[f64()], |build, args| {
let d = build.unary(Opcode::UIToFP, args[0], f64());
build.ret(&[d]);
});
let before = printed(&func, &mut names);
floats(&mut func);
assert_eq!(printed(&func, &mut names), before);
let (mut names, mut func) = one(&[f64()], &[Type::int(64)], |build, args| {
let n = build.unary(Opcode::FPToUI, args[0], Type::int(64));
build.ret(&[n]);
});
let before = printed(&func, &mut names);
floats(&mut func);
assert_eq!(printed(&func, &mut names), before);
}
#[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 }
}
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);
}
}