use rucc_ir::{
Block, Def, Extra, Flags, Func, Imm, Inst, InstData, IntPred, Opcode, Type, Value, ValueList,
};
use crate::uses::count;
use crate::{Analyses, Analysis, 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.without(Analysis::Liveness)
}
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))
.or_else(|| widened_bits(func, inst, &uses))
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: Option<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: Some(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;
}
let widens = ty.bits() < func[left].ty.bits();
let pred = if kind == Opcode::ZExt && widens { pred.unsigned() } else { pred };
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)?),
};
let extra = Extra::IntPred(pred);
Some(Redo { opcode: Opcode::ICmp, extra, ty, lhs: Plan::Already(narrow), rhs: Some(rhs) })
}
fn widened_bits(func: &Func, inst: Inst, uses: &[u32]) -> Option<Redo> {
let (wide, back) = asked(func, inst, uses)?;
let data = &func[wide];
if !bit_at_a_time(data.opcode) {
return None;
}
if func[data.results().next()?].ty.bits() <= 1 {
return None;
}
let args = &func[data.args];
let (&left, &right) = (args.first()?, args.get(1)?);
let readers = if left == right { 2 } else { 1 };
let lhs = side(func, left, uses, readers)?;
let rhs = side(func, right, uses, readers)?;
if matches!((&lhs, &rhs), (Plan::Constant(_), Plan::Constant(_))) {
return None;
}
let extra = Extra::None;
let bit = Redo { opcode: data.opcode, extra, ty: Type::int(1), lhs, rhs: Some(rhs) };
let Some(ty) = back else { return Some(bit) };
let lhs = Plan::Nested(Box::new(bit));
Some(Redo { opcode: Opcode::ZExt, extra, ty, lhs, rhs: None })
}
fn asked(func: &Func, inst: Inst, uses: &[u32]) -> Option<(Inst, Option<Type>)> {
let data = &func[inst];
let args = &func[data.args];
match data.opcode {
Opcode::ICmp if data.extra == Extra::IntPred(IntPred::Ne) => {
let (&left, &right) = (args.first()?, args.get(1)?);
let (zero, wide) = constant(func, right)?;
(zero.signed(wide) == 0).then_some((read_by(func, left, uses, 1)?, None))
}
Opcode::ZExt | Opcode::SExt => {
let ty = func[data.results().next()?].ty;
Some((read_by(func, *args.first()?, uses, 1)?, Some(ty)))
}
_ => None,
}
}
fn side(func: &Func, value: Value, uses: &[u32], readers: u32) -> Option<Plan> {
if let Some((imm, wide)) = constant(func, value) {
let k = imm.signed(wide);
return (k == 0 || k == 1).then_some(Plan::Constant(k));
}
Some(Plan::Already(widened_bit(func, value, uses, readers)?))
}
fn read_by(func: &Func, value: Value, uses: &[u32], readers: u32) -> Option<Inst> {
if uses[value.index()] != readers {
return None;
}
let Def::Result { inst, .. } = func[value].def else { return None };
Some(inst)
}
const fn bit_at_a_time(opcode: Opcode) -> bool {
matches!(opcode, Opcode::And | Opcode::Or | Opcode::Xor)
}
fn widened_bit(func: &Func, value: Value, uses: &[u32], readers: u32) -> Option<Value> {
let inst = read_by(func, value, uses, readers)?;
let data = &func[inst];
if data.opcode != Opcode::ZExt {
return None;
}
let narrow = *func[data.args].first()?;
(func[narrow].ty == Type::int(1)).then_some(narrow)
}
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 operands = operands(func, inst, redo, uses);
for value in func[func[inst].args].iter().copied() {
uses[value.index()] -= 1;
}
let args = listed(func, operands, uses);
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 operands = operands(func, before, redo, uses);
let args = listed(func, operands, uses);
let data = InstData { args, extra: redo.extra, ..InstData::new(redo.opcode) };
written(func, before, data, redo.ty, uses)
}
}
}
fn operands(
func: &mut Func,
before: Inst,
redo: &Redo,
uses: &mut Vec<u32>,
) -> (Value, Option<Value>) {
let lhs = build(func, before, redo.ty, &redo.lhs, uses);
let rhs = redo.rhs.as_ref().map(|plan| build(func, before, redo.ty, plan, uses));
(lhs, rhs)
}
fn listed(func: &mut Func, (lhs, rhs): (Value, Option<Value>), uses: &mut [u32]) -> ValueList {
uses[lhs.index()] += 1;
let Some(rhs) = rhs else { return func.push_values(&[lhs]) };
uses[rhs.index()] += 1;
func.push_values(&[lhs, rhs])
}
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::{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 under(func: &Func, value: Value) -> Value {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("a result") };
*func[func[inst].args].first().expect("an operand")
}
fn predicate(func: &Func, value: Value) -> IntPred {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("a result") };
let rucc_ir::Extra::IntPred(pred) = func[inst].extra else { panic!("a comparison") };
pred
}
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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(),
&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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(),
&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() {
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!(
Narrow
.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited()
)
.changed(),
"{pred}"
);
assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)], "{pred}");
}
}
#[test]
fn a_signed_comparison_of_two_zero_extensions_narrows_to_the_unsigned_one() {
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]);
Narrow.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited(),
);
assert_eq!(predicate(&func, answer), pred.unsigned(), "{pred}");
}
}
#[test]
fn a_signed_comparison_of_two_sign_extensions_keeps_the_predicate_it_was_written_with() {
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]);
Narrow.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited(),
);
assert_eq!(predicate(&func, answer), pred, "{pred}");
}
}
#[test]
fn a_signed_comparison_of_a_zero_extension_against_a_constant_narrows_to_the_unsigned_one() {
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::ZExt, a, Type::int(32));
let k = build.iconst(Type::int(32), 200);
let answer = build.icmp(IntPred::Slt, wide, k);
build.ret(&[answer]);
assert!(
Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(shape(&func, answer).1, vec![Type::int(8), Type::int(8)]);
assert_eq!(predicate(&func, answer), IntPred::Ult);
}
#[test]
fn a_signed_comparison_of_a_zero_extension_against_a_negative_constant_is_left_alone() {
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::ZExt, a, Type::int(32));
let k = build.iconst(Type::int(32), -1);
let answer = build.icmp(IntPred::Sgt, wide, k);
build.ret(&[answer]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[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 crate::machine::fixtures::analyses(),
&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 crate::machine::fixtures::analyses(),
&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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &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 a_bitwise_operation_on_two_widened_bits_is_done_at_one_bit() {
for opcode in [Opcode::And, Opcode::Or, Opcode::Xor] {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(opcode, wide_p, wide_q, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
assert!(
Narrow
.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited()
)
.changed(),
"{opcode:?}"
);
assert_eq!(shape(&func, answer), (opcode, vec![Type::int(1), Type::int(1)]));
}
}
#[test]
fn asking_whether_a_bitwise_operation_on_widened_bits_is_zero_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::And, wide_p, wide_q, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Eq, both, zero);
build.ret(&[answer]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(shape(&func, answer).1, vec![Type::int(32), Type::int(32)]);
}
#[test]
fn a_sum_of_two_widened_bits_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::Add, wide_p, wide_q, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn a_bitwise_operation_on_something_wider_than_a_bit_is_not_this_shape() {
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 both = build.binary(Opcode::And, wide_a, wide_b, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
Narrow.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
assert_eq!(shape(&func, answer).0, Opcode::ICmp);
}
#[test]
fn a_bitwise_operation_on_a_widened_bit_and_a_bit_constant_is_done_at_one_bit() {
for (opcode, k) in
[(Opcode::And, 0), (Opcode::And, 1), (Opcode::Or, 0), (Opcode::Or, 1), (Opcode::Xor, 0)]
{
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let bit = build.iconst(Type::int(32), k);
let both = build.binary(opcode, wide_p, bit, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
assert!(
Narrow
.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited()
)
.changed(),
"{opcode:?} {k}"
);
assert_eq!(shape(&func, answer), (opcode, vec![Type::int(1), Type::int(1)]));
}
}
#[test]
fn a_bitwise_operation_against_a_constant_wider_than_a_bit_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let two = build.iconst(Type::int(32), 2);
let both = build.binary(Opcode::Or, wide_p, two, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn a_widened_bit_the_operation_reads_twice_is_still_only_read_by_it() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let both = build.binary(Opcode::And, wide_p, wide_p, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer]);
assert!(
Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(shape(&func, answer), (Opcode::And, vec![Type::int(1), Type::int(1)]));
}
#[test]
fn a_widened_bit_that_something_else_reads_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::And, wide_p, wide_q, Flags::NONE);
let zero = build.iconst(Type::int(32), 0);
let answer = build.icmp(IntPred::Ne, both, zero);
build.ret(&[answer, wide_p]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn a_bitwise_operation_on_widened_bits_compared_against_one_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::Or, wide_p, wide_q, Flags::NONE);
let one = build.iconst(Type::int(32), 1);
let answer = build.icmp(IntPred::Ne, both, one);
build.ret(&[answer]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn a_bitwise_operation_on_widened_bits_taken_wider_is_done_at_one_bit() {
for kind in [Opcode::ZExt, Opcode::SExt] {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::And, wide_p, wide_q, Flags::NONE);
let wider = build.unary(kind, both, Type::int(64));
build.ret(&[wider]);
assert!(
Narrow
.run(
&mut func,
&mut crate::machine::fixtures::analyses(),
&mut Fuel::unlimited()
)
.changed(),
"{kind:?}"
);
assert_eq!(shape(&func, wider), (Opcode::ZExt, vec![Type::int(1)]), "{kind:?}");
let bit = under(&func, wider);
let want = (Opcode::And, vec![Type::int(1), Type::int(1)]);
assert_eq!(shape(&func, bit), want, "{kind:?}");
}
}
#[test]
fn a_bitwise_operation_on_two_bit_constants_is_left_to_the_folder() {
let (mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
let one = build.iconst(Type::int(32), 1);
let both = build.binary(Opcode::And, zero, one, Flags::NONE);
let wider = build.unary(Opcode::ZExt, both, Type::int(64));
build.ret(&[wider]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn a_bitwise_operation_already_at_one_bit_is_not_done_again() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::Xor, wide_p, wide_q, Flags::NONE);
let wider = build.unary(Opcode::SExt, both, Type::int(64));
build.ret(&[wider]);
let mut an = crate::machine::fixtures::analyses();
assert!(Narrow.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
assert!(!Narrow.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
}
#[test]
fn a_bit_constant_comes_over_under_an_extension_too() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let one = build.iconst(Type::int(32), 1);
let both = build.binary(Opcode::And, wide_p, one, Flags::NONE);
let wider = build.unary(Opcode::SExt, both, Type::int(64));
build.ret(&[wider]);
assert!(
Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(shape(&func, wider), (Opcode::ZExt, vec![Type::int(1)]));
assert_eq!(shape(&func, under(&func, wider)), (Opcode::And, vec![Type::int(1); 2]));
}
#[test]
fn an_extension_of_a_bitwise_operation_on_bytes_is_left_alone() {
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 both = build.binary(Opcode::And, wide_a, wide_b, Flags::NONE);
let wider = build.unary(Opcode::SExt, both, Type::int(64));
build.ret(&[wider]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[test]
fn an_extension_of_a_sum_of_two_widened_bits_is_left_alone() {
let (mut func, block) = blank();
let p = func.append_param(block, Type::int(1));
let q = func.append_param(block, Type::int(1));
let mut build = Builder::new(&mut func, block);
let wide_p = build.unary(Opcode::ZExt, p, Type::int(32));
let wide_q = build.unary(Opcode::ZExt, q, Type::int(32));
let both = build.binary(Opcode::Add, wide_p, wide_q, Flags::NONE);
let wider = build.unary(Opcode::SExt, both, Type::int(64));
build.ret(&[wider]);
assert!(
!Narrow
.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
}
#[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 crate::machine::fixtures::analyses(), &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 crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
.changed()
);
assert_eq!(left(&func, block), 2);
assert_eq!(func[last(&func, block)].opcode, Opcode::Return);
}
}