use std::collections::HashMap;
use std::sync::OnceLock;
use rucc_ir::term::{PLAIN, Plan, Shown, Term, Terms};
use rucc_ir::{Block, Def, Extra, Flags, Func, Imm, Inst, InstData, Opcode, Type, Value};
use crate::rules::{Match, Piece, Table, canonical, identities, strength};
use crate::uses::count;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const FLIPPED: &str = "comparison negated by an exclusive or rewritten as the opposite comparison";
const NO_FUEL: &str = "negated comparison left alone, the pass ran out of fuel";
const NO_FUEL_RULE: &str = "rewrite left alone, the pass ran out of fuel";
const PLANS: [Plan; 3] =
[[Shown::Reg, Shown::Const, Shown::Reg], [Shown::Const, Shown::Reg, Shown::Reg], PLAIN];
const CANONICAL: [Plan; 1] = [[Shown::Const, Shown::Var, Shown::Reg]];
const TABLES: [(&Table, &[Plan]); 3] =
[(&identities::TABLE, &PLANS), (&strength::TABLE, &PLANS), (&canonical::TABLE, &CANONICAL)];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Simplify;
impl Pass for Simplify {
fn name(&self) -> &'static str {
"simplify"
}
fn describe(&self) -> &'static str {
"the identities, the strength reductions, the canonicalisations, and a negated comparison \
as the opposite one"
}
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 forward: HashMap<Value, Value> = HashMap::new();
let uses = count(func);
let dead = |func: &Func, inst: Inst| match func[inst].first_result {
Some(result) => uses[result.index()] == 0,
None => false,
};
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
if dead(func, inst) {
continue;
}
if let Some(flip) = negated_comparison(func, inst) {
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
let args = func.push_values(&[flip.lhs, flip.rhs]);
let data = &mut func[inst];
data.opcode = flip.opcode;
data.flags = flip.flags;
data.args = args;
data.extra = flip.extra;
stats.optimized(FLIPPED);
continue;
}
let Some((rewrite, pattern)) = identity(func, inst) else { continue };
if !fuel.take() {
stats.missed(NO_FUEL_RULE);
continue;
}
match rewrite {
Rewrite::Value(value) => {
let result = func[inst].first_result.expect("the rule matched a result");
forward.insert(result, value);
}
Rewrite::Constant(number) => become_constant(func, inst, number),
Rewrite::Built { opcode, lhs, rhs } => {
become_instruction(func, inst, opcode, lhs, rhs);
}
}
stats.optimized(pattern);
}
}
if !forward.is_empty() {
substitute(func, &forward);
}
stats
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Rewrite {
Value(Value),
Constant(i128),
Built {
opcode: Opcode,
lhs: Operand,
rhs: Operand,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Operand {
Value(Value),
Constant(i128),
}
fn identity(func: &Func, inst: Inst) -> Option<(Rewrite, &'static str)> {
let result = func[inst].first_result?;
for (table, plan) in
TABLES.into_iter().flat_map(|(table, plans)| plans.iter().map(move |&plan| (table, plan)))
{
let terms = Terms::new(func, inst, plan);
let Some(found) = table.find(&terms, Term::Root) else { continue };
let rule = table.rule(&found);
let rewrite = match rule.replacement {
[Piece::App { head, arity: 1 }, Piece::Var { index, .. }]
if head.starts_with("value.") =>
{
match found.bindings.get(*index) {
Some(&Term::Reg(value)) => Rewrite::Value(value),
_ => continue,
}
}
[Piece::App { head, arity: 1 }, Piece::Int(number)]
if head.starts_with("iconst.") && func[result].ty.is_int() =>
{
Rewrite::Constant(*number)
}
pieces => match built(pieces, &found) {
Some(rewrite) => rewrite,
None => continue,
},
};
return Some((rewrite, rule.pattern));
}
None
}
fn built(pieces: &'static [Piece], found: &Match<Term>) -> Option<Rewrite> {
let [Piece::App { head, arity: 2 }, rest @ ..] = pieces else { return None };
let opcode = opcode_of(head)?;
let (lhs, rest) = operand(rest, found)?;
let (rhs, rest) = operand(rest, found)?;
rest.is_empty().then_some(Rewrite::Built { opcode, lhs, rhs })
}
fn operand(pieces: &'static [Piece], found: &Match<Term>) -> Option<(Operand, &'static [Piece])> {
match pieces {
[Piece::App { head, arity: 1 }, Piece::Var { index, .. }, rest @ ..]
if head.starts_with("value.") =>
{
match found.bindings.get(*index) {
Some(&Term::Reg(value)) => Some((Operand::Value(value), rest)),
_ => None,
}
}
[Piece::App { head, arity: 1 }, Piece::Int(number), rest @ ..]
if head.starts_with("iconst.") =>
{
Some((Operand::Constant(*number), rest))
}
[Piece::App { head, arity: 1 }, Piece::Var { index, .. }, rest @ ..]
if head.starts_with("iconst.") =>
{
match found.bindings.get(*index) {
Some(&Term::Num(number)) => Some((Operand::Constant(number), rest)),
_ => None,
}
}
_ => None,
}
}
fn opcode_of(head: &str) -> Option<Opcode> {
static NAMES: OnceLock<HashMap<&'static str, Opcode>> = OnceLock::new();
let names = NAMES.get_or_init(|| {
let mut names = HashMap::new();
for (opcode, name) in rucc_ir::term::heads() {
names.entry(name).or_insert(opcode);
}
names
});
names.get(head).copied()
}
fn become_instruction(func: &mut Func, inst: Inst, opcode: Opcode, lhs: Operand, rhs: Operand) {
let result = func[inst].first_result.expect("the rule matched a result");
let ty = func[result].ty;
let lhs = defined(func, inst, ty, lhs);
let rhs = defined(func, inst, ty, rhs);
let args = func.push_values(&[lhs, rhs]);
let data = &mut func[inst];
data.opcode = opcode;
data.args = args;
data.extra = Extra::None;
data.flags = Flags::NONE;
}
fn defined(func: &mut Func, before: Inst, ty: Type, operand: Operand) -> Value {
match operand {
Operand::Value(value) => value,
Operand::Constant(number) => {
let at = func.add_imm(Imm::int(number, ty.lane()));
let data = InstData { extra: Extra::Imm(at), ..InstData::new(Opcode::IConst) };
let span = func.span(before);
let iconst = func.create_inst(data, &[ty], span);
func.insert_before(iconst, before);
func[iconst].first_result.expect("one result was asked for")
}
}
}
fn become_constant(func: &mut Func, inst: Inst, number: i128) {
let result = func[inst].first_result.expect("the rule matched a result");
let ty = func[result].ty;
let imm = func.add_imm(Imm::int(number, ty.lane()));
let args = func.push_values(&[]);
let data = &mut func[inst];
data.opcode = Opcode::IConst;
data.args = args;
data.extra = Extra::Imm(imm);
data.flags = Flags::NONE;
}
fn chase(forward: &HashMap<Value, Value>, value: Value) -> Value {
let mut value = value;
while let Some(&next) = forward.get(&value) {
value = next;
}
value
}
fn substitute(func: &mut Func, forward: &HashMap<Value, Value>) {
let with = |value: Value| chase(forward, value);
for block in func.blocks().collect::<Vec<Block>>() {
for inst in func.insts(block).collect::<Vec<Inst>>() {
let args = func[inst].args;
func.rewrite(args, with);
for call in func.successors(inst).collect::<Vec<_>>() {
func.rewrite(call.args, with);
}
}
}
}
struct Flip {
opcode: Opcode,
flags: Flags,
extra: Extra,
lhs: Value,
rhs: Value,
}
fn negated_comparison(func: &Func, inst: Inst) -> Option<Flip> {
let data = &func[inst];
if data.opcode != Opcode::Xor {
return None;
}
let args = &func[data.args];
let (&first, &second) = (args.first()?, args.get(1)?);
if func[first].ty != Type::int(1) {
return None;
}
let cmp = match (all_ones(func, first), all_ones(func, second)) {
(true, false) => second,
(false, true) => first,
_ => return None,
};
let Def::Result { inst: cmp, .. } = func[cmp].def else { return None };
let data = &func[cmp];
let extra = match (data.opcode, data.extra) {
(Opcode::ICmp, Extra::IntPred(pred)) => Extra::IntPred(pred.inverse()),
(Opcode::FCmp, Extra::FloatPred(pred)) => Extra::FloatPred(pred.inverse()),
_ => return None,
};
let args = &func[data.args];
Some(Flip {
opcode: data.opcode,
flags: data.flags,
extra,
lhs: *args.first()?,
rhs: *args.get(1)?,
})
}
fn all_ones(func: &Func, value: Value) -> bool {
let ty = func[value].ty;
let Def::Result { inst, .. } = func[value].def else { return false };
let data = &func[inst];
let Extra::Imm(at) = data.extra else { return false };
if data.opcode != Opcode::IConst {
return false;
}
func[at].signed(ty) == -1
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Block, Builder, Extra, Flags, Float, FloatPred, Func, IntPred, Module, Opcode, Signature,
Type, Value,
};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::{CANONICAL, PLANS, Shown, TABLES, canonical, identities, strength};
use crate::rules::Piece;
use crate::stats::Kind;
use crate::{Analyses, Fuel, Pass, simplify::Simplify};
fn blank() -> (Interner, Func, Block) {
let mut names = Interner::new();
let name = names.intern("f");
let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(1)]));
let block = func.create_block();
(names, func, block)
}
fn one_block(ty: Type) -> (Interner, Func, Block) {
let mut names = Interner::new();
let name = names.intern("f");
let signature = Signature::new().with_params(&[ty]).with_returns(&[ty]);
let mut func = Func::new(name, signature);
let block = func.create_block();
(names, func, block)
}
fn simplify(func: &mut Func) -> bool {
Simplify.run(func, &mut Analyses::new(), &mut Fuel::unlimited()).changed()
}
fn came_from(func: &Func, value: Value) -> (Opcode, Extra) {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("not a result") };
(func[inst].opcode, func[inst].extra)
}
fn returned(func: &Func, block: Block) -> Value {
let inst = func.terminator(block).expect("the block has a terminator");
func[func[inst].args][0]
}
fn operands(func: &Func, value: Value) -> Vec<Value> {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("not a result") };
func[func[inst].args].to_vec()
}
fn number(func: &Func, value: Value) -> i128 {
let rucc_ir::Def::Result { inst, .. } = func[value].def else { panic!("not a result") };
let data = &func[inst];
assert_eq!(data.opcode, Opcode::IConst, "not a constant");
let Extra::Imm(at) = data.extra else { panic!("a constant with no number") };
func[at].signed(func[value].ty)
}
#[test]
fn every_rule_leaves_a_shape_the_pass_knows_what_to_do_with() {
for (table, _) in TABLES {
for rule in table.rules {
let known = matches!(
rule.replacement,
[Piece::App { head, arity: 1 }, Piece::Var { .. }]
if head.starts_with("value.")
) || matches!(
rule.replacement,
[Piece::App { head, arity: 1 }, Piece::Int(_)]
if head.starts_with("iconst.")
) || matches!(
rule.replacement,
[Piece::App { arity: 2, .. }, ..] if instruction(rule.replacement)
);
assert!(known, "{} leaves a shape the pass would skip", rule.pattern);
}
}
}
fn instruction(pieces: &'static [Piece]) -> bool {
let [Piece::App { head, arity: 2 }, rest @ ..] = pieces else { return false };
if super::opcode_of(head).is_none() {
return false;
}
let operand = |pieces: &'static [Piece]| match pieces {
[Piece::App { head, arity: 1 }, Piece::Var { .. }, rest @ ..]
if head.starts_with("value.") =>
{
Some(rest)
}
[Piece::App { head, arity: 1 }, Piece::Int(_), rest @ ..]
if head.starts_with("iconst.") =>
{
Some(rest)
}
[Piece::App { head, arity: 1 }, Piece::Var { .. }, rest @ ..]
if head.starts_with("iconst.") =>
{
Some(rest)
}
_ => None,
};
operand(rest).and_then(operand).is_some_and(<[Piece]>::is_empty)
}
#[test]
fn each_table_holds_every_rule_its_file_writes() {
let tier_one = include_str!("../rules/simplify.rules");
let tier_two = include_str!("../rules/strength.rules");
let tier_three = include_str!("../rules/canonical.rules");
let count = |text: &str| text.matches("(rule (simplify ").count();
assert_eq!(identities::TABLE.rules.len(), count(tier_one));
assert_eq!(strength::TABLE.rules.len(), count(tier_two));
assert_eq!(canonical::TABLE.rules.len(), count(tier_three));
assert!(
identities::TABLE.rules.len() > 100,
"tier one is about a hundred rules and there are fewer"
);
assert!(
strength::TABLE.rules.len() > 20,
"tier two is the multiplications and the divisions and there are fewer"
);
assert_eq!(
canonical::TABLE.rules.len(),
20,
"tier three is five commutative operators at four widths"
);
}
#[test]
fn a_pattern_is_reached_by_one_of_the_plans() {
assert_eq!(PLANS.len(), 3);
}
#[test]
fn a_canonicalisation_is_only_matched_with_the_right_operand_refused() {
let (_, plans) = TABLES[2];
assert_eq!(plans.len(), 1);
assert_eq!(plans[0], CANONICAL[0]);
assert_eq!(plans[0][1], Shown::Var);
for plan in PLANS {
assert_ne!(plan, plans[0], "a shared plan would let a canonicalisation cycle");
}
}
#[test]
fn a_constant_on_the_left_of_a_commutative_operation_moves_to_the_right() {
for opcode in [Opcode::Add, Opcode::Mul, Opcode::And, Opcode::Or, Opcode::Xor] {
for width in [8, 16, 32, 64] {
let ty = Type::int(width);
let (_, mut func, block) = one_block(ty);
let x = func.append_param(block, ty);
let mut build = Builder::new(&mut func, block);
let three = build.iconst(ty, 3);
let value = build.binary(opcode, three, x, Flags::NONE);
build.ret(&[value]);
assert!(simplify(&mut func), "{opcode:?} at i{width} was left alone");
let args = operands(&func, returned(&func, block));
assert_eq!(came_from(&func, returned(&func, block)).0, opcode);
assert_eq!(args[0], x, "{opcode:?} at i{width} kept the value on the right");
assert_eq!(number(&func, args[1]), 3, "{opcode:?} at i{width} lost its constant");
}
}
}
#[test]
fn an_operation_on_two_constants_is_not_swapped_back_and_forth() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let mut build = Builder::new(&mut func, block);
let three = build.iconst(i32, 3);
let five = build.iconst(i32, 5);
let sum = build.binary(Opcode::Add, three, five, Flags::NONE);
build.ret(&[sum]);
assert!(!simplify(&mut func), "the constants were rearranged rather than left to folding");
let args = operands(&func, returned(&func, block));
assert_eq!(number(&func, args[0]), 3);
assert_eq!(number(&func, args[1]), 5);
}
#[test]
fn a_constant_already_on_the_right_is_left_alone() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let three = build.iconst(i32, 3);
let sum = build.binary(Opcode::Add, x, three, Flags::NONE);
build.ret(&[sum]);
assert!(!simplify(&mut func));
let args = operands(&func, returned(&func, block));
assert_eq!(args[0], x);
assert_eq!(number(&func, args[1]), 3);
}
#[test]
fn a_subtraction_keeps_its_operands_where_they_are() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let three = build.iconst(i32, 3);
let difference = build.binary(Opcode::Sub, three, x, Flags::NONE);
build.ret(&[difference]);
assert!(!simplify(&mut func));
let args = operands(&func, returned(&func, block));
assert_eq!(number(&func, args[0]), 3);
assert_eq!(args[1], x);
}
#[test]
fn adding_nothing_points_every_reader_at_the_operand() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let sum = build.binary(Opcode::Add, x, zero, Flags::NONE);
build.ret(&[sum]);
assert!(simplify(&mut func));
assert_eq!(returned(&func, block), x);
assert_eq!(came_from(&func, sum).0, Opcode::Add);
}
#[test]
fn the_constant_is_found_on_either_side_of_an_identity() {
for swapped in [false, true] {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let (lhs, rhs) = if swapped { (zero, x) } else { (x, zero) };
let sum = build.binary(Opcode::Add, lhs, rhs, Flags::NONE);
build.ret(&[sum]);
assert!(simplify(&mut func), "swapped {swapped}");
assert_eq!(returned(&func, block), x, "swapped {swapped}");
}
}
#[test]
fn multiplying_by_nothing_becomes_the_constant_where_it_stands() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let product = build.binary(Opcode::Mul, x, zero, Flags::NONE);
build.ret(&[product]);
assert!(simplify(&mut func));
assert_eq!(returned(&func, block), product);
assert_eq!(came_from(&func, product).0, Opcode::IConst);
assert_eq!(number(&func, product), 0);
}
#[test]
fn a_value_against_itself() {
for bits in [8, 16, 32, 64] {
let ty = Type::int(bits);
let (_, mut func, block) = one_block(ty);
let x = func.append_param(block, ty);
let mut build = Builder::new(&mut func, block);
let both = build.binary(Opcode::And, x, x, Flags::NONE);
build.ret(&[both]);
assert!(simplify(&mut func), "{bits} bits");
assert_eq!(returned(&func, block), x, "{bits} bits");
let (_, mut func, block) = one_block(ty);
let x = func.append_param(block, ty);
let mut build = Builder::new(&mut func, block);
let nothing = build.binary(Opcode::Sub, x, x, Flags::NONE);
build.ret(&[nothing]);
assert!(simplify(&mut func), "{bits} bits");
assert_eq!(number(&func, nothing), 0, "{bits} bits");
}
}
#[test]
fn dividing_by_one_and_the_remainder_that_goes_with_it() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let one = build.iconst(i32, 1);
let quotient = build.binary(Opcode::SDiv, x, one, Flags::NONE);
let rest = build.binary(Opcode::SRem, x, one, Flags::NONE);
let sum = build.binary(Opcode::Add, quotient, rest, Flags::NONE);
build.ret(&[sum]);
assert!(simplify(&mut func));
assert_eq!(number(&func, rest), 0);
let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("not a result") };
assert_eq!(func[func[inst].args][0], x);
}
#[test]
fn all_ones_at_one_bit_is_the_one_the_front_end_writes() {
for written in [-1, 1] {
let bit = Type::int(1);
let (_, mut func, block) = one_block(bit);
let x = func.append_param(block, bit);
let mut build = Builder::new(&mut func, block);
let ones = build.iconst(bit, written);
let kept = build.binary(Opcode::And, x, ones, Flags::NONE);
build.ret(&[kept]);
assert!(simplify(&mut func), "written as {written}");
assert_eq!(returned(&func, block), x, "written as {written}");
}
}
#[test]
fn one_identity_feeding_another_is_followed_to_the_end() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let one = build.iconst(i32, 1);
let sum = build.binary(Opcode::Add, x, zero, Flags::NONE);
let product = build.binary(Opcode::Mul, sum, one, Flags::NONE);
let shifted = build.binary(Opcode::Shl, product, zero, Flags::NONE);
build.ret(&[shifted]);
assert!(simplify(&mut func));
assert_eq!(returned(&func, block), x);
}
#[test]
fn an_instruction_no_rule_is_about_is_left_alone() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let three = build.iconst(i32, 3);
let tripled = build.binary(Opcode::Mul, x, three, Flags::NONE);
build.ret(&[tripled]);
assert!(!simplify(&mut func), "no rule is about multiplying by three");
assert_eq!(returned(&func, block), tripled);
assert_eq!(came_from(&func, tripled).0, Opcode::Mul);
}
#[test]
fn multiplying_by_two_becomes_an_addition_of_the_value_with_itself() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let two = build.iconst(i32, 2);
let doubled = build.binary(Opcode::Mul, x, two, Flags::NONE);
build.ret(&[doubled]);
assert!(simplify(&mut func));
assert_eq!(returned(&func, block), doubled);
assert_eq!(came_from(&func, doubled).0, Opcode::Add);
assert_eq!(operands(&func, doubled), [x, x]);
}
#[test]
fn multiplying_by_minus_one_becomes_a_subtraction_from_a_zero_the_rewrite_defines() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let minus = build.iconst(i32, -1);
let negated = build.binary(Opcode::Mul, x, minus, Flags::NONE);
build.ret(&[negated]);
assert!(simplify(&mut func));
assert_eq!(returned(&func, block), negated);
assert_eq!(came_from(&func, negated).0, Opcode::Sub);
let args = operands(&func, negated);
assert_eq!(number(&func, args[0]), 0);
assert_eq!(args[1], x);
}
#[test]
fn the_flags_of_the_instruction_a_strength_reduction_replaces_do_not_come_with_it() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let two = build.iconst(i32, 2);
let doubled = build.binary(Opcode::Mul, x, two, Flags::NSW);
build.ret(&[doubled]);
assert!(simplify(&mut func));
let rucc_ir::Def::Result { inst, .. } = func[doubled].def else { panic!("not a result") };
assert_eq!(func[inst].flags, Flags::NONE);
}
#[test]
fn a_strength_reduction_leaves_the_verifier_nothing_to_complain_about() {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let i32 = Type::int(32);
let (mut names, mut func, block) = one_block(i32);
let mut module = Module::new(names.intern("test.c"), &target);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let minus = build.iconst(i32, -1);
let negated = build.binary(Opcode::Mul, x, minus, Flags::NONE);
let two = build.iconst(i32, 2);
let doubled = build.binary(Opcode::Mul, negated, two, Flags::NONE);
build.ret(&[doubled]);
assert!(simplify(&mut func));
module.add_func(func);
rucc_ir::verify(&module, &names).expect("the pass left the function verifiable");
}
#[test]
fn the_pass_leaves_the_verifier_nothing_to_complain_about() {
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let i32 = Type::int(32);
let (mut names, mut func, block) = one_block(i32);
let mut module = Module::new(names.intern("test.c"), &target);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let one = build.iconst(i32, 1);
let sum = build.binary(Opcode::Add, x, zero, Flags::NONE);
let product = build.binary(Opcode::Mul, sum, one, Flags::NONE);
let gone = build.binary(Opcode::Sub, product, product, Flags::NONE);
let total = build.binary(Opcode::Add, product, gone, Flags::NONE);
build.ret(&[total]);
assert!(simplify(&mut func));
module.add_func(func);
rucc_ir::verify(&module, &names).expect("the pass left the function verifiable");
}
#[test]
fn fuel_stops_an_identity_and_not_the_walk() {
let i32 = Type::int(32);
let (_, mut func, block) = one_block(i32);
let x = func.append_param(block, i32);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(i32, 0);
let first = build.binary(Opcode::Add, x, zero, Flags::NONE);
let second = build.binary(Opcode::Sub, x, zero, Flags::NONE);
let sum = build.binary(Opcode::Add, first, second, Flags::NONE);
build.ret(&[sum]);
let stats = Simplify.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
assert!(stats.changed());
assert_eq!(stats.total(Kind::Optimized), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL_RULE), 1);
let rucc_ir::Def::Result { inst, .. } = func[sum].def else { panic!("not a result") };
assert_eq!(func[func[inst].args], [x, second]);
}
#[test]
fn a_negated_float_comparison_becomes_the_opposite_predicate() {
for pred in FloatPred::all() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(64), 0);
let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
let cmp = build.fcmp(pred, x, x, Flags::NONE);
let ones = build.iconst(Type::int(1), -1);
let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
build.ret(&[not]);
assert!(simplify(&mut func), "{pred:?}");
assert_eq!(
came_from(&func, not),
(Opcode::FCmp, Extra::FloatPred(pred.inverse())),
"{pred:?}"
);
}
}
#[test]
fn a_negated_integer_comparison_becomes_the_opposite_predicate() {
for pred in IntPred::all() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let cmp = build.icmp(pred, x, x);
let ones = build.iconst(Type::int(1), -1);
let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
build.ret(&[not]);
assert!(simplify(&mut func), "{pred:?}");
assert_eq!(
came_from(&func, not),
(Opcode::ICmp, Extra::IntPred(pred.inverse())),
"{pred:?}"
);
}
}
#[test]
fn the_constant_is_found_on_either_side() {
for swapped in [false, true] {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let cmp = build.icmp(IntPred::Slt, x, x);
let ones = build.iconst(Type::int(1), -1);
let (lhs, rhs) = if swapped { (ones, cmp) } else { (cmp, ones) };
let not = build.binary(Opcode::Xor, lhs, rhs, Flags::NONE);
build.ret(&[not]);
assert!(simplify(&mut func), "swapped {swapped}");
assert_eq!(came_from(&func, not).1, Extra::IntPred(IntPred::Sge));
}
}
#[test]
fn an_exclusive_or_of_two_comparisons_is_left_alone() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let a = build.icmp(IntPred::Slt, x, x);
let b = build.icmp(IntPred::Sgt, x, x);
let differ = build.binary(Opcode::Xor, a, b, Flags::NONE);
build.ret(&[differ]);
assert!(!simplify(&mut func));
assert_eq!(came_from(&func, differ).0, Opcode::Xor);
}
#[test]
fn an_exclusive_or_of_something_that_is_not_a_comparison_is_left_alone() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let narrow = build.unary(Opcode::Trunc, x, Type::int(1));
let ones = build.iconst(Type::int(1), -1);
let not = build.binary(Opcode::Xor, narrow, ones, Flags::NONE);
build.ret(&[not]);
assert!(!simplify(&mut func));
assert_eq!(came_from(&func, not).0, Opcode::Xor);
}
#[test]
fn a_wider_exclusive_or_with_one_is_not_a_negation_and_is_left_alone() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let cmp = build.icmp(IntPred::Slt, x, x);
let wide = build.unary(Opcode::ZExt, cmp, Type::int(32));
let one = build.iconst(Type::int(32), 1);
let flipped = build.binary(Opcode::Xor, wide, one, Flags::NONE);
let narrow = build.unary(Opcode::Trunc, flipped, Type::int(1));
build.ret(&[narrow]);
assert!(!simplify(&mut func), "an i32 xor 1 flips one bit of thirty two");
assert_eq!(came_from(&func, flipped).0, Opcode::Xor);
}
#[test]
fn the_comparisons_flags_travel_with_the_predicate() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(64), 0);
let x = build.unary(Opcode::Bitcast, x, Type::float(Float::F64));
let cmp = build.fcmp(FloatPred::Olt, x, x, Flags::FAST);
let ones = build.iconst(Type::int(1), -1);
let not = build.binary(Opcode::Xor, cmp, ones, Flags::NONE);
build.ret(&[not]);
assert!(simplify(&mut func));
let rucc_ir::Def::Result { inst, .. } = func[not].def else { panic!("not a result") };
assert_eq!(func[inst].flags, Flags::FAST);
}
#[test]
fn fuel_stops_the_transformation_and_not_the_walk() {
let (_, mut func, block) = blank();
let mut build = Builder::new(&mut func, block);
let x = build.iconst(Type::int(32), 3);
let a = build.icmp(IntPred::Slt, x, x);
let b = build.icmp(IntPred::Sgt, x, x);
let ones = build.iconst(Type::int(1), -1);
let first = build.binary(Opcode::Xor, a, ones, Flags::NONE);
let second = build.binary(Opcode::Xor, b, ones, Flags::NONE);
let both = build.binary(Opcode::And, first, second, Flags::NONE);
build.ret(&[both]);
let stats = Simplify.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
assert!(stats.changed());
assert_eq!(stats.count(Kind::Optimized, super::FLIPPED), 1);
assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
assert_eq!(came_from(&func, first).0, Opcode::ICmp);
assert_eq!(came_from(&func, second).0, Opcode::Xor);
}
}