use rucc_ir::{Block, Def, Extra, Flags, Func, Imm, Inst, InstData, Opcode, Type, Value};
use crate::uses::count;
use crate::{Analyses, Fuel, Pass, Preserved, Stats};
const NARROWED: &str = "arithmetic redone at the width the program truncates it to";
const NO_FUEL: &str = "arithmetic left wide, the pass ran out of fuel";
const DEPTH: u32 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Narrow;
impl Pass for Narrow {
fn name(&self) -> &'static str {
"narrow"
}
fn describe(&self) -> &'static str {
"arithmetic the program truncates is redone at the width it truncates to"
}
fn preserves(&self) -> Preserved {
Preserved::ALL
}
fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let mut uses = count(func);
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
let Some(redo) = truncated_arithmetic(func, inst, &uses)
.or_else(|| extended_comparison(func, inst))
else {
continue;
};
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
apply(func, inst, &redo, &mut uses);
stats.optimized(NARROWED);
}
}
stats
}
}
struct Redo {
opcode: Opcode,
extra: Extra,
ty: Type,
lhs: Plan,
rhs: Plan,
}
enum Plan {
Already(Value),
Constant(i128),
Nested(Box<Redo>),
}
fn truncated_arithmetic(func: &Func, inst: Inst, uses: &[u32]) -> Option<Redo> {
let data = &func[inst];
if data.opcode != Opcode::Trunc {
return None;
}
let ty = func[data.results().next()?].ty;
if !narrowable(ty) {
return None;
}
redo(func, *func[data.args].first()?, ty, uses, DEPTH)
}
const fn narrowable(ty: Type) -> bool {
ty.is_int() && ty.is_scalar() && ty.bits() >= 8
}
fn redo(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Redo> {
if depth == 0 || uses[value.index()] != 1 {
return None;
}
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
if !low_bits_only(data.opcode) {
return None;
}
let args = &func[data.args];
let (&left, &right) = (args.first()?, args.get(1)?);
let lhs = plan(func, left, ty, uses, depth)?;
let rhs = match data.opcode {
Opcode::Shl => Plan::Constant(count_below(func, right, ty)?),
_ => plan(func, right, ty, uses, depth)?,
};
Some(Redo { opcode: data.opcode, extra: Extra::None, ty, lhs, rhs })
}
fn plan(func: &Func, value: Value, ty: Type, uses: &[u32], depth: u32) -> Option<Plan> {
if let Some(narrow) = extended(func, value, ty) {
return Some(Plan::Already(narrow));
}
if let Some((imm, wide)) = constant(func, value) {
return Some(Plan::Constant(imm.signed(wide)));
}
redo(func, value, ty, uses, depth - 1).map(|redo| Plan::Nested(Box::new(redo)))
}
const fn low_bits_only(opcode: Opcode) -> bool {
matches!(
opcode,
Opcode::Add
| Opcode::Sub
| Opcode::Mul
| Opcode::And
| Opcode::Or
| Opcode::Xor
| Opcode::Shl
)
}
fn extended_comparison(func: &Func, inst: Inst) -> Option<Redo> {
let data = &func[inst];
if data.opcode != Opcode::ICmp {
return None;
}
let Extra::IntPred(pred) = data.extra else { return None };
let args = &func[data.args];
let (&left, &right) = (args.first()?, args.get(1)?);
let (kind, ty, narrow) = widening(func, left)?;
if !narrowable(ty) {
return None;
}
if kind == Opcode::ZExt && pred.is_signed() {
return None;
}
let rhs = match widening(func, right) {
Some((same, from, other)) if same == kind && from == ty => Plan::Already(other),
_ => Plan::Constant(survives(func, right, kind, ty)?),
};
Some(Redo { opcode: Opcode::ICmp, extra: data.extra, ty, lhs: Plan::Already(narrow), rhs })
}
fn widening(func: &Func, value: Value) -> Option<(Opcode, Type, Value)> {
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
if data.opcode != Opcode::SExt && data.opcode != Opcode::ZExt {
return None;
}
let narrow = *func[data.args].first()?;
Some((data.opcode, func[narrow].ty, narrow))
}
fn extended(func: &Func, value: Value, ty: Type) -> Option<Value> {
let (_, from, narrow) = widening(func, value)?;
(from == ty).then_some(narrow)
}
fn constant(func: &Func, value: Value) -> Option<(Imm, Type)> {
let Def::Result { inst, .. } = func[value].def else { return None };
let data = &func[inst];
let Extra::Imm(at) = data.extra else { return None };
if data.opcode != Opcode::IConst {
return None;
}
let ty = func[value].ty;
ty.is_int().then(|| (func[at], ty))
}
fn count_below(func: &Func, value: Value, ty: Type) -> Option<i128> {
let (imm, wide) = constant(func, value)?;
let by = imm.signed(wide);
(by >= 0 && by < i128::from(ty.bits())).then_some(by)
}
fn survives(func: &Func, value: Value, kind: Opcode, ty: Type) -> Option<i128> {
let (imm, wide) = constant(func, value)?;
let k = imm.signed(wide);
let back = Imm::int(k, ty).signed(ty);
let same = if kind == Opcode::SExt { back } else { Imm::int(k, ty).unsigned() as i128 };
(same == k).then_some(k)
}
fn apply(func: &mut Func, inst: Inst, redo: &Redo, uses: &mut Vec<u32>) {
let lhs = build(func, inst, redo.ty, &redo.lhs, uses);
let rhs = build(func, inst, redo.ty, &redo.rhs, uses);
for value in func[func[inst].args].iter().copied() {
uses[value.index()] -= 1;
}
let args = func.push_values(&[lhs, rhs]);
uses[lhs.index()] += 1;
uses[rhs.index()] += 1;
let data = &mut func[inst];
data.opcode = redo.opcode;
data.flags = Flags::NONE;
data.args = args;
data.extra = redo.extra;
}
fn build(func: &mut Func, before: Inst, ty: Type, plan: &Plan, uses: &mut Vec<u32>) -> Value {
match plan {
Plan::Already(value) => *value,
Plan::Constant(value) => {
let at = func.add_imm(Imm::int(*value, ty.lane()));
let data = InstData { extra: Extra::Imm(at), ..InstData::new(Opcode::IConst) };
written(func, before, data, ty, uses)
}
Plan::Nested(redo) => {
let lhs = build(func, before, redo.ty, &redo.lhs, uses);
let rhs = build(func, before, redo.ty, &redo.rhs, uses);
let args = func.push_values(&[lhs, rhs]);
uses[lhs.index()] += 1;
uses[rhs.index()] += 1;
let data = InstData { args, extra: redo.extra, ..InstData::new(redo.opcode) };
written(func, before, data, redo.ty, uses)
}
}
}
fn written(func: &mut Func, before: Inst, data: InstData, ty: Type, uses: &mut Vec<u32>) -> Value {
let span = func.span(before);
let inst = func.create_inst(data, &[ty], span);
func.insert_before(inst, before);
uses.resize(func.counts().values, 0);
func[inst].first_result.expect("one result was asked for")
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Block, Builder, Flags, Func, Inst, IntPred, Opcode, Signature, Type, Value};
use crate::narrow::Narrow;
use crate::{Analyses, Fuel, Pass};
fn blank() -> (Func, Block) {
let mut names = Interner::new();
let name = names.intern("f");
let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
let block = func.create_block();
(func, block)
}
fn shape(func: &Func, value: Value) -> (Opcode, Vec<Type>) {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("a result") };
let data = &func[inst];
(data.opcode, func[data.args].iter().map(|&arg| func[arg].ty).collect())
}
fn left(func: &Func, block: Block) -> usize {
func.insts(block).count()
}
fn last(func: &Func, block: Block) -> Inst {
func.insts(block).last().expect("a block with something in it")
}
#[test]
fn a_truncated_sum_of_two_extensions_is_the_sum_at_the_narrow_width() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
build.ret(&[narrow]);
assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
assert_eq!(left(&func, block), 5);
}
#[test]
fn a_constant_operand_is_written_down_again_at_the_narrow_width() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide = build.unary(Opcode::SExt, a, Type::int(32));
let one = build.iconst(Type::int(32), 1);
let sum = build.binary(Opcode::Add, wide, one, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
build.ret(&[narrow]);
assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow), (Opcode::Add, vec![Type::int(8), Type::int(8)]));
}
#[test]
fn a_chain_of_arithmetic_narrows_the_whole_way_down() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let c = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let wide_c = build.unary(Opcode::SExt, c, Type::int(32));
let inner = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
let outer = build.binary(Opcode::Mul, inner, wide_c, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, outer, Type::int(8));
build.ret(&[narrow]);
assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow), (Opcode::Mul, vec![Type::int(8), Type::int(8)]));
assert_eq!(left(&func, block), 8);
}
#[test]
fn an_operation_something_else_reads_stays_wide() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
let kept = build.unary(Opcode::SExt, narrow, Type::int(32));
build.ret(&[sum, kept]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
}
#[test]
fn a_divide_stays_wide_because_the_narrow_one_can_raise() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let quotient = build.binary(Opcode::SDiv, wide_a, wide_b, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, quotient, Type::int(8));
build.ret(&[narrow]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow), (Opcode::Trunc, vec![Type::int(32)]));
}
#[test]
fn a_shift_by_a_constant_below_the_width_narrows_and_one_at_it_does_not() {
for (by, narrows) in [(3, true), (20, false)] {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide = build.unary(Opcode::SExt, a, Type::int(32));
let count = build.iconst(Type::int(32), by);
let shifted = build.binary(Opcode::Shl, wide, count, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
build.ret(&[narrow]);
assert_eq!(
Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
narrows,
"shift by {by}"
);
let want = if narrows { Opcode::Shl } else { Opcode::Trunc };
assert_eq!(shape(&func, narrow).0, want, "shift by {by}");
}
}
#[test]
fn a_shift_by_a_value_stays_wide() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let n = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide = build.unary(Opcode::SExt, a, Type::int(32));
let by = build.unary(Opcode::SExt, n, Type::int(32));
let shifted = build.binary(Opcode::Shl, wide, by, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, shifted, Type::int(8));
build.ret(&[narrow]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, narrow).0, Opcode::Trunc);
}
#[test]
fn a_comparison_of_two_sign_extensions_is_the_comparison_of_what_they_extended() {
for pred in IntPred::all() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let answer = build.icmp(pred, wide_a, wide_b);
build.ret(&[answer]);
assert!(
Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
"{pred}"
);
assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)], "{pred}");
}
}
#[test]
fn a_comparison_of_two_zero_extensions_narrows_at_every_predicate_but_the_signed_ones() {
for pred in IntPred::all() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::ZExt, a, Type::int(32));
let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
let answer = build.icmp(pred, wide_a, wide_b);
build.ret(&[answer]);
assert_eq!(
Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
!pred.is_signed(),
"{pred}"
);
}
}
#[test]
fn a_comparison_against_a_constant_narrows_when_the_constant_is_one_of_the_narrow_ones() {
for (k, narrows) in [(120, true), (-1, true), (200, false)] {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide = build.unary(Opcode::SExt, a, Type::int(32));
let k = build.iconst(Type::int(32), k);
let answer = build.icmp(IntPred::Eq, wide, k);
build.ret(&[answer]);
assert_eq!(
Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
narrows
);
}
}
#[test]
fn one_extension_against_the_other_kind_is_not_a_comparison_at_the_narrow_width() {
for pred in IntPred::all() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::ZExt, b, Type::int(32));
let answer = build.icmp(pred, wide_a, wide_b);
build.ret(&[answer]);
assert!(
!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed(),
"{pred}"
);
}
}
#[test]
fn a_truth_is_not_a_width_to_narrow_to() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide = build.unary(Opcode::ZExt, a, Type::int(32));
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, wide, zero);
build.ret(&[answer]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(shape(&func, answer).1, vec![Type::int(32), Type::int(32)]);
}
#[test]
fn extensions_from_different_widths_are_not_a_comparison_at_either_of_them() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(16));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let answer = build.icmp(IntPred::Slt, wide_a, wide_b);
build.ret(&[answer]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
}
#[test]
fn the_overflow_flags_do_not_come_along() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let sum = build.binary(Opcode::Add, wide_a, wide_b, Flags::NSW);
let narrow = build.unary(Opcode::Trunc, sum, Type::int(8));
build.ret(&[narrow]);
assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
let rucc_ir::Def::Result { inst, .. } = func[narrow].def else { panic!("a result") };
assert_eq!(func[inst].flags, Flags::NONE);
}
#[test]
fn fuel_stops_the_narrowing_and_not_the_looking() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(8));
let b = func.append_param(block, Type::int(8));
let mut build = Builder::new(&mut func, block);
let wide_a = build.unary(Opcode::SExt, a, Type::int(32));
let wide_b = build.unary(Opcode::SExt, b, Type::int(32));
let first = build.icmp(IntPred::Slt, wide_a, wide_b);
let second = build.icmp(IntPred::Sgt, wide_a, wide_b);
build.ret(&[first, second]);
let mut fuel = Fuel::of(1);
assert!(Narrow.run(&mut func, &mut Analyses::new(), &mut fuel).changed());
assert_eq!(shape(&func, first).1, vec![Type::int(8), Type::int(8)]);
assert_eq!(shape(&func, second).1, vec![Type::int(32), Type::int(32)]);
}
#[test]
fn a_block_that_narrows_nothing_is_left_exactly_as_it_was() {
let (mut func, block) = blank();
let a = func.append_param(block, Type::int(32));
let mut build = Builder::new(&mut func, block);
let sum = build.binary(Opcode::Add, a, a, Flags::NONE);
build.ret(&[sum]);
assert!(!Narrow.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited()).changed());
assert_eq!(left(&func, block), 2);
assert_eq!(func[last(&func, block)].opcode, Opcode::Return);
}
}