use rucc_ir::{
BlockCall, Builder, Extra, Flags, Func, Imm, Inst, InstData, IntPred, Opcode, 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]);
}
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 super::{blocks_for, floats, 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);
}
}