use rucc_base::Interner;
use rucc_ir::{
CallInfo, Extra, Flags, Float, FloatPred, Func, Imm, Inst, InstData, IntPred, MemInfo,
MemOrder, Opcode, Restrict, Signature, Type, Value,
};
const QUAD: Float = Float::F128;
const BITS: u32 = 128;
const NARROW: u32 = 32;
const WORD: u32 = 64;
pub fn calls(func: &mut Func, names: &mut Interner) {
let found: Vec<Inst> =
func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
for inst in found {
match func[inst].opcode {
Opcode::FAdd | Opcode::FSub | Opcode::FMul | Opcode::FDiv => {
arithmetic(func, names, inst);
}
Opcode::FNeg => negate(func, names, inst),
Opcode::FCmp => compare(func, names, inst),
Opcode::FConst => constant(func, inst),
Opcode::FPExt => widen(func, names, inst),
Opcode::FPTrunc => narrow(func, names, inst),
Opcode::SIToFP | Opcode::UIToFP => from_integer(func, names, inst),
Opcode::FPToSI | Opcode::FPToUI => to_integer(func, names, inst),
_ => {}
}
}
}
fn quad(ty: Type) -> bool {
ty.is_scalar() && ty.format() == Some(QUAD)
}
fn produced(func: &Func, inst: Inst) -> Option<Type> {
func[inst].first_result.map(|value| func[value].ty)
}
fn arithmetic(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
if !quad(ty) {
return;
}
let args = func[func[inst].args].to_vec();
let [a, b] = args[..] else { return };
let routine = match func[inst].opcode {
Opcode::FAdd => "__addtf3",
Opcode::FSub => "__subtf3",
Opcode::FMul => "__multf3",
Opcode::FDiv => "__divtf3",
_ => return,
};
into_call(func, names, inst, routine, &[a, b]);
}
fn negate(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Some(&arg) = func[func[inst].args].first() else { return };
if !quad(ty) {
return;
}
into_call(func, names, inst, "__negtf2", &[arg]);
}
fn compare(func: &mut Func, names: &mut Interner, inst: Inst) {
let args = func[func[inst].args].to_vec();
let [a, b] = args[..] else { return };
if !quad(func[a].ty) || !quad(func[b].ty) {
return;
}
let Extra::FloatPred(pred) = func[inst].extra else { return };
if let Some((routine, test)) = single(pred) {
let answer = call(func, names, inst, routine, &[a, b], Type::int(NARROW));
let zero = ahead_const(func, inst, Imm::int(0, Type::int(NARROW)), Type::int(NARROW));
let extra = Extra::IntPred(test);
becomes(func, inst, Opcode::ICmp, extra, &[answer, zero]);
return;
}
if let FloatPred::False | FloatPred::True = pred {
let bits = u128::from(pred == FloatPred::True);
let extra = Extra::Imm(func.add_imm(Imm::int(bits as i128, Type::I1)));
becomes(func, inst, Opcode::IConst, extra, &[]);
return;
}
let (FloatPred::One | FloatPred::Ueq) = pred else { return };
let ordered = pair(func, names, inst, "__unordtf2", a, b, IntPred::Eq);
let different = pair(func, names, inst, "__netf2", a, b, IntPred::Ne);
let (opcode, args) = if pred == FloatPred::One {
(Opcode::And, [ordered, different])
} else {
let unordered = flipped(func, inst, ordered);
let same = flipped(func, inst, different);
(Opcode::Or, [unordered, same])
};
becomes(func, inst, opcode, Extra::None, &args);
}
fn single(pred: FloatPred) -> Option<(&'static str, IntPred)> {
Some(match pred {
FloatPred::Oeq => ("__eqtf2", IntPred::Eq),
FloatPred::Une => ("__netf2", IntPred::Ne),
FloatPred::Olt => ("__lttf2", IntPred::Slt),
FloatPred::Ole => ("__letf2", IntPred::Sle),
FloatPred::Ogt => ("__gttf2", IntPred::Sgt),
FloatPred::Oge => ("__getf2", IntPred::Sge),
FloatPred::Uno => ("__unordtf2", IntPred::Ne),
FloatPred::Ord => ("__unordtf2", IntPred::Eq),
FloatPred::Ult => ("__getf2", IntPred::Slt),
FloatPred::Ule => ("__gttf2", IntPred::Sle),
FloatPred::Ugt => ("__letf2", IntPred::Sgt),
FloatPred::Uge => ("__lttf2", IntPred::Sge),
_ => return None,
})
}
fn pair(
func: &mut Func,
names: &mut Interner,
inst: Inst,
routine: &str,
a: Value,
b: Value,
test: IntPred,
) -> Value {
let answer = call(func, names, inst, routine, &[a, b], Type::int(NARROW));
let zero = ahead_const(func, inst, Imm::int(0, Type::int(NARROW)), Type::int(NARROW));
let args = func.push_values(&[answer, zero]);
let extra = Extra::IntPred(test);
written(func, inst, InstData { args, extra, ..InstData::new(Opcode::ICmp) }, Type::I1)
}
fn flipped(func: &mut Func, inst: Inst, value: Value) -> Value {
let one = ahead_const(func, inst, Imm::int(1, Type::I1), Type::I1);
let args = func.push_values(&[value, one]);
written(func, inst, InstData { args, ..InstData::new(Opcode::Xor) }, Type::I1)
}
fn constant(func: &mut Func, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Extra::Imm(imm) = func[inst].extra else { return };
if !quad(ty) {
return;
}
let bits = func[imm].bits();
let bytes = u64::from(BITS / 8);
let whole = MemInfo {
size: bytes,
align: BITS / 8,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let slot = {
let extra = Extra::Mem(func.add_mem(whole));
written(func, inst, InstData { extra, ..InstData::new(Opcode::Alloca) }, Type::PTR)
};
let half = u64::from(WORD / 8);
let word = Type::int(WORD);
let low = ahead_const(func, inst, Imm::int(bits as i128, word), word);
write(func, inst, low, slot, MemInfo { size: half, ..whole });
let step = ahead_const(func, inst, Imm::int(half as i128, word), word);
let args = func.push_values(&[slot, step]);
let above = written(func, inst, InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
let high = ahead_const(func, inst, Imm::int((bits >> WORD) as i128, word), word);
write(func, inst, high, above, MemInfo { size: half, align: WORD / 8, ..whole });
let extra = Extra::Mem(func.add_mem(whole));
becomes(func, inst, Opcode::Load, extra, &[slot]);
}
fn widen(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Some(&arg) = func[func[inst].args].first() else { return };
if !quad(ty) {
return;
}
let routine = match func[arg].ty.format() {
Some(Float::F32) => "__extendsftf2",
Some(Float::F64) => "__extenddftf2",
_ => return,
};
into_call(func, names, inst, routine, &[arg]);
}
fn narrow(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Some(&arg) = func[func[inst].args].first() else { return };
if !quad(func[arg].ty) {
return;
}
let routine = match ty.format() {
Some(Float::F32) => "__trunctfsf2",
Some(Float::F64) => "__trunctfdf2",
_ => return,
};
into_call(func, names, inst, routine, &[arg]);
}
fn from_integer(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Some(&arg) = func[func[inst].args].first() else { return };
let from = func[arg].ty;
if !quad(ty) || !from.is_int() || !from.is_scalar() {
return;
}
let signed = func[inst].opcode == Opcode::SIToFP;
let Some(width) = holder(from.bits()) else { return };
let routine = match (signed, width) {
(true, NARROW) => "__floatsitf",
(false, NARROW) => "__floatunsitf",
(true, _) => "__floatditf",
(false, _) => "__floatunditf",
};
let value = if from.bits() == width {
arg
} else {
let opcode = if signed { Opcode::SExt } else { Opcode::ZExt };
let args = func.push_values(&[arg]);
written(func, inst, InstData { args, ..InstData::new(opcode) }, Type::int(width))
};
into_call(func, names, inst, routine, &[value]);
}
fn to_integer(func: &mut Func, names: &mut Interner, inst: Inst) {
let Some(ty) = produced(func, inst) else { return };
let Some(&arg) = func[func[inst].args].first() else { return };
if !quad(func[arg].ty) || !ty.is_int() || !ty.is_scalar() {
return;
}
let signed = func[inst].opcode == Opcode::FPToSI;
let Some(width) = holder(ty.bits()) else { return };
let routine = match (signed, width) {
(true, NARROW) => "__fixtfsi",
(false, NARROW) => "__fixunstfsi",
(true, _) => "__fixtfdi",
(false, _) => "__fixunstfdi",
};
if ty.bits() == width {
into_call(func, names, inst, routine, &[arg]);
return;
}
let answer = call(func, names, inst, routine, &[arg], Type::int(width));
becomes(func, inst, Opcode::Trunc, Extra::None, &[answer]);
}
fn holder(bits: u32) -> Option<u32> {
match bits {
0..=NARROW => Some(NARROW),
33..=WORD => Some(WORD),
_ => None,
}
}
fn into_call(func: &mut Func, names: &mut Interner, inst: Inst, routine: &str, args: &[Value]) {
let Some(ty) = produced(func, inst) else { return };
let params: Vec<Type> = args.iter().map(|&value| func[value].ty).collect();
let signature = func.add_signature(Signature::new().with_params(¶ms).with_returns(&[ty]));
let callee = Some(names.intern(routine));
let varargs = func.push_abis(&[]);
let extra = Extra::Call(func.add_call(CallInfo { callee, signature, varargs }));
becomes(func, inst, Opcode::Call, extra, args);
}
fn call(
func: &mut Func,
names: &mut Interner,
inst: Inst,
routine: &str,
args: &[Value],
ty: Type,
) -> Value {
let params: Vec<Type> = args.iter().map(|&value| func[value].ty).collect();
let signature = func.add_signature(Signature::new().with_params(¶ms).with_returns(&[ty]));
let callee = Some(names.intern(routine));
let varargs = func.push_abis(&[]);
let extra = Extra::Call(func.add_call(CallInfo { callee, signature, varargs }));
let args = func.push_values(args);
written(func, inst, InstData { args, extra, ..InstData::new(Opcode::Call) }, 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 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 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, extra: Extra, args: &[Value]) {
let args = func.push_values(args);
let data = &mut func[inst];
data.opcode = opcode;
data.args = args;
data.extra = extra;
data.flags = data.flags.intersection(Flags::legal_on(opcode));
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Block, Builder, Module, Signature};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::{BITS, Flags, Float, FloatPred, Func, Opcode, Type, Value, calls};
fn quad() -> Type {
Type::float(Float::F128)
}
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("q.c"), &target());
rucc_ir::print_func(&module, func, names)
}
fn shell(names: &mut Interner, params: &[Type], returns: &[Type]) -> (Func, Block, Vec<Value>) {
let signature = Signature::new().with_params(params).with_returns(returns);
let mut func = Func::new(names.intern("f"), signature);
let entry = func.create_block();
let values = params.iter().map(|&ty| func.append_param(entry, ty)).collect();
(func, entry, values)
}
fn binary(opcode: Opcode) -> String {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[quad(), quad()], &[quad()]);
let mut build = Builder::new(&mut func, entry);
let answer = build.binary(opcode, params[0], params[1], Flags::NONE);
build.ret(&[answer]);
calls(&mut func, &mut names);
printed(&func, &mut names)
}
fn compared(pred: FloatPred) -> String {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[quad(), quad()], &[Type::I1]);
let mut build = Builder::new(&mut func, entry);
let answer = build.fcmp(pred, params[0], params[1], Flags::NONE);
build.ret(&[answer]);
calls(&mut func, &mut names);
printed(&func, &mut names)
}
#[test]
fn the_four_operations_are_the_four_routines() {
for (opcode, routine) in [
(Opcode::FAdd, "__addtf3"),
(Opcode::FSub, "__subtf3"),
(Opcode::FMul, "__multf3"),
(Opcode::FDiv, "__divtf3"),
] {
let text = binary(opcode);
assert!(text.contains(&format!("@{routine}")), "{routine}: {text}");
assert_eq!(text.matches(" = f").count(), 0, "no float arithmetic left: {text}");
assert_eq!(text.matches(" = call").count(), 1, "one call: {text}");
}
}
#[test]
fn a_negation_is_the_routine_rather_than_a_sign_flip() {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[quad()], &[quad()]);
let mut build = Builder::new(&mut func, entry);
let answer = build.unary(Opcode::FNeg, params[0], quad());
build.ret(&[answer]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(text.contains("@__negtf2"), "{text}");
assert!(!text.contains("xor"), "no sign flip in a register: {text}");
}
#[test]
fn an_ordered_comparison_is_its_own_routine_tested_against_zero() {
for (pred, routine, test) in [
(FloatPred::Oeq, "__eqtf2", "icmp eq"),
(FloatPred::Une, "__netf2", "icmp ne"),
(FloatPred::Olt, "__lttf2", "icmp slt"),
(FloatPred::Ole, "__letf2", "icmp sle"),
(FloatPred::Ogt, "__gttf2", "icmp sgt"),
(FloatPred::Oge, "__getf2", "icmp sge"),
] {
let text = compared(pred);
assert!(text.contains(&format!("@{routine}")), "{routine}: {text}");
assert!(text.contains(test), "{test}: {text}");
assert!(!text.contains("fcmp"), "the comparison is gone: {text}");
}
}
#[test]
fn an_unordered_comparison_is_the_opposite_routine_read_the_same_way() {
for (pred, routine, test) in [
(FloatPred::Ult, "__getf2", "icmp slt"),
(FloatPred::Ule, "__gttf2", "icmp sle"),
(FloatPred::Ugt, "__letf2", "icmp sgt"),
(FloatPred::Uge, "__lttf2", "icmp sge"),
] {
let text = compared(pred);
assert!(text.contains(&format!("@{routine}")), "{routine}: {text}");
assert!(text.contains(test), "{test}: {text}");
}
}
#[test]
fn whether_two_values_can_be_ordered_at_all_is_one_routine_either_way_round() {
let unordered = compared(FloatPred::Uno);
assert!(unordered.contains("@__unordtf2"), "{unordered}");
assert!(unordered.contains("icmp ne"), "{unordered}");
let ordered = compared(FloatPred::Ord);
assert!(ordered.contains("@__unordtf2"), "{ordered}");
assert!(ordered.contains("icmp eq"), "{ordered}");
}
#[test]
fn ordered_and_different_is_two_calls_joined() {
let text = compared(FloatPred::One);
assert!(text.contains("@__unordtf2"), "{text}");
assert!(text.contains("@__netf2"), "{text}");
assert_eq!(text.matches(" = call").count(), 2, "both calls: {text}");
assert_eq!(text.matches(" = and").count(), 1, "joined: {text}");
assert!(!text.contains("xor"), "nothing is negated: {text}");
}
#[test]
fn unordered_or_equal_is_the_negation_of_it() {
let text = compared(FloatPred::Ueq);
assert_eq!(text.matches(" = call").count(), 2, "both calls: {text}");
assert_eq!(text.matches(" = or").count(), 1, "joined the other way: {text}");
assert_eq!(text.matches(" = xor").count(), 2, "both answers negated: {text}");
}
#[test]
fn the_two_comparisons_with_no_operands_to_read_are_constants() {
let never = compared(FloatPred::False);
assert!(never.contains("iconst.i1 0"), "{never}");
assert!(!never.contains("call"), "nothing is called: {never}");
let always = compared(FloatPred::True);
assert!(always.contains("iconst.i1 -1"), "{always}");
}
#[test]
fn a_constant_goes_through_the_frame_a_word_at_a_time() {
let mut names = Interner::new();
let (mut func, entry, _) = shell(&mut names, &[], &[quad()]);
let mut build = Builder::new(&mut func, entry);
let value = build.fconst(quad(), (3u128 << 64) | 5);
build.ret(&[value]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(!text.contains("fconst"), "the constant is gone: {text}");
assert_eq!(text.matches("alloca").count(), 1, "one slot: {text}");
assert_eq!(text.matches("store").count(), 2, "a word at a time: {text}");
assert!(text.contains("iconst.i64 5"), "the low word first: {text}");
assert!(text.contains("iconst.i64 3"), "the high word above it: {text}");
assert_eq!(text.matches("ptr_add").count(), 1, "the high word is eight bytes up: {text}");
assert_eq!(text.matches(" = load").count(), 1, "read back as one value: {text}");
}
#[test]
fn the_two_narrower_formats_are_a_routine_each_way() {
for (from, to, routine) in [
(Float::F32, Float::F128, "__extendsftf2"),
(Float::F64, Float::F128, "__extenddftf2"),
(Float::F128, Float::F32, "__trunctfsf2"),
(Float::F128, Float::F64, "__trunctfdf2"),
] {
let mut names = Interner::new();
let (mut func, entry, params) =
shell(&mut names, &[Type::float(from)], &[Type::float(to)]);
let mut build = Builder::new(&mut func, entry);
let opcode = if to == Float::F128 { Opcode::FPExt } else { Opcode::FPTrunc };
let answer = build.unary(opcode, params[0], Type::float(to));
build.ret(&[answer]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(text.contains(&format!("@{routine}")), "{routine}: {text}");
}
}
#[test]
fn a_narrow_integer_is_widened_before_the_conversion() {
for (opcode, bits, extend, routine) in [
(Opcode::SIToFP, 16, " = sext", "__floatsitf"),
(Opcode::UIToFP, 16, " = zext", "__floatunsitf"),
(Opcode::SIToFP, 32, "", "__floatsitf"),
(Opcode::UIToFP, 64, "", "__floatunditf"),
] {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[Type::int(bits)], &[quad()]);
let mut build = Builder::new(&mut func, entry);
let answer = build.unary(opcode, params[0], quad());
build.ret(&[answer]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(text.contains(&format!("@{routine}")), "{routine}: {text}");
if extend.is_empty() {
assert!(!text.contains(" = sext"), "nothing to widen: {text}");
assert!(!text.contains(" = zext"), "nothing to widen: {text}");
} else {
assert!(text.contains(extend), "{extend}: {text}");
}
}
}
#[test]
fn a_narrow_answer_is_the_wider_routine_and_a_truncation() {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[quad()], &[Type::int(16)]);
let mut build = Builder::new(&mut func, entry);
let answer = build.unary(Opcode::FPToSI, params[0], Type::int(16));
build.ret(&[answer]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(text.contains("@__fixtfsi"), "{text}");
assert_eq!(text.matches(" = trunc").count(), 1, "cut down afterwards: {text}");
}
#[test]
fn a_conversion_against_a_wide_integer_is_left_exactly_as_it_was() {
let mut names = Interner::new();
let (mut func, entry, params) = shell(&mut names, &[Type::int(BITS)], &[quad()]);
let mut build = Builder::new(&mut func, entry);
let answer = build.unary(Opcode::SIToFP, params[0], quad());
build.ret(&[answer]);
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(!text.contains("call"), "no routine is called: {text}");
assert!(text.contains("sitofp"), "the conversion is still there to be refused: {text}");
}
#[test]
fn the_narrower_formats_go_past_untouched() {
let mut names = Interner::new();
let double = Type::float(Float::F64);
let (mut func, entry, params) = shell(&mut names, &[double, double], &[double]);
let mut build = Builder::new(&mut func, entry);
let sum = build.binary(Opcode::FAdd, params[0], params[1], Flags::NONE);
let answer = build.fcmp(FloatPred::Olt, sum, params[1], Flags::NONE);
build.ret(&[sum]);
let _ = answer;
calls(&mut func, &mut names);
let text = printed(&func, &mut names);
assert!(!text.contains("call"), "nothing became a call: {text}");
assert!(text.contains("fadd"), "the add is still an add: {text}");
assert!(text.contains("fcmp"), "the comparison is still a comparison: {text}");
}
}