From 2828da1f5603e226345c82ec540d01d518d1572f Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Fri, 1 May 2020 15:45:01 -0700 Subject: [PATCH] peepmatic: Introduce the `peepmatic-test` crate This crate provides testing utilities for `peepmatic`, and a test-only instruction set we can use to check that various optimizations do or don't apply. --- cranelift/peepmatic/crates/test/Cargo.toml | 14 + cranelift/peepmatic/crates/test/src/lib.rs | 531 ++++++++++++++++++ .../peepmatic/crates/test/tests/tests.rs | 393 +++++++++++++ 3 files changed, 938 insertions(+) create mode 100644 cranelift/peepmatic/crates/test/Cargo.toml create mode 100644 cranelift/peepmatic/crates/test/src/lib.rs create mode 100644 cranelift/peepmatic/crates/test/tests/tests.rs diff --git a/cranelift/peepmatic/crates/test/Cargo.toml b/cranelift/peepmatic/crates/test/Cargo.toml new file mode 100644 index 0000000000..bca85f8bf8 --- /dev/null +++ b/cranelift/peepmatic/crates/test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "peepmatic-test" +version = "0.1.0" +authors = ["Nick Fitzgerald "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +env_logger = "0.7.1" +log = "0.4.8" +peepmatic = { path = "../.." } +peepmatic-runtime = { path = "../runtime" } diff --git a/cranelift/peepmatic/crates/test/src/lib.rs b/cranelift/peepmatic/crates/test/src/lib.rs new file mode 100644 index 0000000000..c43ce89957 --- /dev/null +++ b/cranelift/peepmatic/crates/test/src/lib.rs @@ -0,0 +1,531 @@ +//! Testing utilities and a testing-only instruction set for `peepmatic`. + +#![deny(missing_debug_implementations)] + +use peepmatic_runtime::{ + cc::ConditionCode, + instruction_set::InstructionSet, + operator::Operator, + part::{Constant, Part}, + paths::Path, + r#type::{BitWidth, Kind, Type}, +}; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Instruction(pub usize); + +#[derive(Debug)] +pub struct InstructionData { + pub operator: Operator, + pub r#type: Type, + pub immediates: Vec, + pub arguments: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Immediate { + Constant(Constant), + ConditionCode(ConditionCode), +} + +impl Immediate { + fn unwrap_constant(&self) -> Constant { + match *self { + Immediate::Constant(c) => c, + _ => panic!("not a constant"), + } + } +} + +impl From for Immediate { + fn from(c: Constant) -> Immediate { + Immediate::Constant(c) + } +} + +impl From for Immediate { + fn from(cc: ConditionCode) -> Immediate { + Immediate::ConditionCode(cc) + } +} + +impl From for Part { + fn from(imm: Immediate) -> Part { + match imm { + Immediate::Constant(c) => Part::Constant(c), + Immediate::ConditionCode(cc) => Part::ConditionCode(cc), + } + } +} + +impl TryFrom> for Immediate { + type Error = &'static str; + + fn try_from(part: Part) -> Result { + match part { + Part::Constant(c) => Ok(Immediate::Constant(c)), + Part::ConditionCode(c) => Ok(Immediate::ConditionCode(c)), + Part::Instruction(_) => Err("instruction parts cannot be converted into immediates"), + } + } +} + +#[derive(Debug, Default)] +pub struct Program { + instr_counter: usize, + instruction_data: BTreeMap, + replacements: RefCell>, +} + +impl Program { + /// Are `a` and `b` structurally equivalent, even if they use different + /// `Instruction`s for various arguments? + pub fn structurally_eq(&mut self, a: Instruction, b: Instruction) -> bool { + macro_rules! ensure_eq { + ($a:expr, $b:expr) => {{ + let a = &$a; + let b = &$b; + if a != b { + log::debug!( + "{} != {} ({:?} != {:?})", + stringify!($a), + stringify!($b), + a, + b + ); + return false; + } + }}; + } + + let a = self.resolve(a); + let b = self.resolve(b); + if a == b { + return true; + } + + let a = self.data(a); + let b = self.data(b); + log::debug!("structurally_eq({:?}, {:?})", a, b); + + ensure_eq!(a.operator, b.operator); + ensure_eq!(a.r#type, b.r#type); + ensure_eq!(a.immediates, b.immediates); + ensure_eq!(a.arguments.len(), b.arguments.len()); + a.arguments + .clone() + .into_iter() + .zip(b.arguments.clone().into_iter()) + .all(|(a, b)| self.structurally_eq(a, b)) + } + + pub fn instructions(&self) -> impl Iterator { + self.instruction_data.iter().map(|(k, v)| (*k, v)) + } + + pub fn replace_instruction(&mut self, old: Instruction, new: Instruction) { + log::debug!("replacing {:?} with {:?}", old, new); + + let old = self.resolve(old); + let new = self.resolve(new); + if old == new { + return; + } + + let mut replacements = self.replacements.borrow_mut(); + let existing_replacement = replacements.insert(old, new); + assert!(existing_replacement.is_none()); + + let old_data = self.instruction_data.remove(&old); + assert!(old_data.is_some()); + } + + pub fn resolve(&self, inst: Instruction) -> Instruction { + let mut replacements = self.replacements.borrow_mut(); + let mut replacements_followed = 0; + let mut resolved = inst; + while let Some(i) = replacements.get(&resolved).cloned() { + log::trace!("resolving replaced instruction: {:?} -> {:?}", resolved, i); + replacements_followed += 1; + assert!( + replacements_followed <= replacements.len(), + "cyclic replacements" + ); + + resolved = i; + continue; + } + + if inst != resolved { + let old_replacement = replacements.insert(inst, resolved); + assert!(old_replacement.is_some()); + } + + resolved + } + + pub fn data(&self, inst: Instruction) -> &InstructionData { + let inst = self.resolve(inst); + &self.instruction_data[&inst] + } + + pub fn new_instruction( + &mut self, + operator: Operator, + r#type: Type, + immediates: Vec, + arguments: Vec, + ) -> Instruction { + assert_eq!( + operator.immediates_arity() as usize, + immediates.len(), + "wrong number of immediates for {:?}: expected {}, found {}", + operator, + operator.immediates_arity(), + immediates.len(), + ); + assert_eq!( + operator.params_arity() as usize, + arguments.len(), + "wrong number of arguments for {:?}: expected {}, found {}", + operator, + operator.params_arity(), + arguments.len(), + ); + + assert!(!r#type.bit_width.is_polymorphic()); + assert!(immediates.iter().all(|imm| match imm { + Immediate::Constant(Constant::Bool(_, w)) + | Immediate::Constant(Constant::Int(_, w)) => !w.is_polymorphic(), + Immediate::ConditionCode(_) => true, + })); + + let inst = Instruction(self.instr_counter); + self.instr_counter += 1; + + let data = InstructionData { + operator, + r#type, + immediates, + arguments, + }; + + log::trace!("new instruction: {:?} = {:?}", inst, data); + self.instruction_data.insert(inst, data); + inst + } + + pub fn r#const(&mut self, c: Constant, root_bit_width: BitWidth) -> Instruction { + assert!(!root_bit_width.is_polymorphic()); + match c { + Constant::Bool(_, bit_width) => self.new_instruction( + Operator::Bconst, + if bit_width.is_polymorphic() { + Type { + kind: Kind::Bool, + bit_width: root_bit_width, + } + } else { + Type { + kind: Kind::Bool, + bit_width, + } + }, + vec![c.into()], + vec![], + ), + Constant::Int(_, bit_width) => self.new_instruction( + Operator::Iconst, + if bit_width.is_polymorphic() { + Type { + kind: Kind::Int, + bit_width: root_bit_width, + } + } else { + Type { + kind: Kind::Int, + bit_width, + } + }, + vec![c.into()], + vec![], + ), + } + } + + fn instruction_to_constant(&mut self, inst: Instruction) -> Option { + match self.data(inst) { + InstructionData { + operator: Operator::Iconst, + immediates, + .. + } => Some(immediates[0].unwrap_constant()), + InstructionData { + operator: Operator::Bconst, + immediates, + .. + } => Some(immediates[0].unwrap_constant()), + _ => None, + } + } + + fn part_to_immediate(&mut self, part: Part) -> Result { + match part { + Part::Instruction(i) => self + .instruction_to_constant(i) + .map(|c| c.into()) + .ok_or("non-constant instructions cannot be converted into immediates"), + Part::Constant(c) => Ok(c.into()), + Part::ConditionCode(cc) => Ok(Immediate::ConditionCode(cc)), + } + } + + fn part_to_instruction( + &mut self, + root: Instruction, + part: Part, + ) -> Result { + match part { + Part::Instruction(inst) => { + let inst = self.resolve(inst); + Ok(inst) + } + Part::Constant(c) => { + let root_width = self.data(root).r#type.bit_width; + Ok(self.r#const(c, root_width)) + } + Part::ConditionCode(_) => Err("condition codes cannot be converted into instructions"), + } + } +} + +#[derive(Debug)] +pub struct TestIsa { + pub native_word_size_in_bits: u8, +} + +impl<'a> InstructionSet<'a> for TestIsa { + type Context = Program; + + type Instruction = Instruction; + + fn replace_instruction( + &self, + program: &mut Program, + old: Instruction, + new: Part, + ) -> Instruction { + log::debug!("replace_instruction({:?}, {:?})", old, new); + let new = program.part_to_instruction(old, new).unwrap(); + program.replace_instruction(old, new); + new + } + + fn get_part_at_path( + &self, + program: &mut Program, + root: Instruction, + path: Path, + ) -> Option> { + log::debug!("get_part_at_path({:?})", path); + + assert!(!path.0.is_empty()); + assert_eq!(path.0[0], 0); + + let mut part = Part::Instruction(root); + for p in &path.0[1..] { + if let Part::Instruction(inst) = part { + let data = program.data(inst); + let p = *p as usize; + + if p < data.immediates.len() { + part = data.immediates[p].into(); + continue; + } + + if let Some(inst) = data.arguments.get(p - data.immediates.len()).copied() { + part = Part::Instruction(inst); + continue; + } + } + + return None; + } + + Some(part) + } + + fn operator(&self, program: &mut Program, instr: Instruction) -> Option { + log::debug!("operator({:?})", instr); + let data = program.data(instr); + Some(data.operator) + } + + fn make_inst_1( + &self, + program: &mut Program, + root: Instruction, + operator: Operator, + r#type: Type, + a: Part, + ) -> Instruction { + log::debug!( + "make_inst_1(\n\toperator = {:?},\n\ttype = {},\n\ta = {:?},\n)", + operator, + r#type, + a, + ); + + let (imms, args) = match operator.immediates_arity() { + 0 => { + assert_eq!(operator.params_arity(), 1); + (vec![], vec![program.part_to_instruction(root, a).unwrap()]) + } + 1 => { + assert_eq!(operator.params_arity(), 0); + (vec![program.part_to_immediate(a).unwrap()], vec![]) + } + _ => unreachable!(), + }; + program.new_instruction(operator, r#type, imms, args) + } + + fn make_inst_2( + &self, + program: &mut Program, + root: Instruction, + operator: Operator, + r#type: Type, + a: Part, + b: Part, + ) -> Instruction { + log::debug!( + "make_inst_2(\n\toperator = {:?},\n\ttype = {},\n\ta = {:?},\n\tb = {:?},\n)", + operator, + r#type, + a, + b, + ); + + let (imms, args) = match operator.immediates_arity() { + 0 => { + assert_eq!(operator.params_arity(), 2); + ( + vec![], + vec![ + program.part_to_instruction(root, a).unwrap(), + program.part_to_instruction(root, b).unwrap(), + ], + ) + } + 1 => { + assert_eq!(operator.params_arity(), 1); + ( + vec![program.part_to_immediate(a).unwrap()], + vec![program.part_to_instruction(root, b).unwrap()], + ) + } + 2 => { + assert_eq!(operator.params_arity(), 0); + ( + vec![ + program.part_to_immediate(a).unwrap(), + program.part_to_immediate(b).unwrap(), + ], + vec![], + ) + } + _ => unreachable!(), + }; + program.new_instruction(operator, r#type, imms, args) + } + + fn make_inst_3( + &self, + program: &mut Program, + root: Instruction, + operator: Operator, + r#type: Type, + a: Part, + b: Part, + c: Part, + ) -> Instruction { + log::debug!( + "make_inst_3(\n\toperator = {:?},\n\ttype = {},\n\ta = {:?},\n\tb = {:?},\n\tc = {:?},\n)", + operator, + r#type, + a, + b, + c, + ); + let (imms, args) = match operator.immediates_arity() { + 0 => { + assert_eq!(operator.params_arity(), 3); + ( + vec![], + vec![ + program.part_to_instruction(root, a).unwrap(), + program.part_to_instruction(root, b).unwrap(), + program.part_to_instruction(root, c).unwrap(), + ], + ) + } + 1 => { + assert_eq!(operator.params_arity(), 2); + ( + vec![program.part_to_immediate(a).unwrap()], + vec![ + program.part_to_instruction(root, b).unwrap(), + program.part_to_instruction(root, c).unwrap(), + ], + ) + } + 2 => { + assert_eq!(operator.params_arity(), 1); + ( + vec![ + program.part_to_immediate(a).unwrap(), + program.part_to_immediate(b).unwrap(), + ], + vec![program.part_to_instruction(root, c).unwrap()], + ) + } + 3 => { + assert_eq!(operator.params_arity(), 0); + ( + vec![ + program.part_to_immediate(a).unwrap(), + program.part_to_immediate(b).unwrap(), + program.part_to_immediate(c).unwrap(), + ], + vec![], + ) + } + _ => unreachable!(), + }; + program.new_instruction(operator, r#type, imms, args) + } + + fn instruction_to_constant( + &self, + program: &mut Program, + inst: Instruction, + ) -> Option { + log::debug!("instruction_to_constant({:?})", inst); + program.instruction_to_constant(inst) + } + + fn instruction_result_bit_width(&self, program: &mut Program, inst: Instruction) -> u8 { + log::debug!("instruction_result_bit_width({:?})", inst); + let ty = program.data(inst).r#type; + ty.bit_width.fixed_width().unwrap() + } + + fn native_word_size_in_bits(&self, _program: &mut Program) -> u8 { + log::debug!("native_word_size_in_bits"); + self.native_word_size_in_bits + } +} diff --git a/cranelift/peepmatic/crates/test/tests/tests.rs b/cranelift/peepmatic/crates/test/tests/tests.rs new file mode 100644 index 0000000000..989424adb8 --- /dev/null +++ b/cranelift/peepmatic/crates/test/tests/tests.rs @@ -0,0 +1,393 @@ +use peepmatic_runtime::{ + cc::ConditionCode, + operator::Operator, + part::Constant, + r#type::{BitWidth, Type}, +}; +use peepmatic_test::*; + +const TEST_ISA: TestIsa = TestIsa { + native_word_size_in_bits: 32, +}; + +macro_rules! optimizer { + ($opts:ident, $source:expr) => {{ + let _ = env_logger::try_init(); + $opts = peepmatic::compile_str($source, std::path::Path::new("peepmatic-test")).unwrap(); + $opts.optimizer(TEST_ISA) + }}; +} + +#[test] +fn opcode() { + let opts; + let mut optimizer = optimizer!(opts, "(=> (iadd $x 0) $x)"); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let zero = program.r#const(Constant::Int(0, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let add = program.new_instruction(Operator::Iadd, Type::i32(), vec![], vec![five, zero]); + + let new = optimizer.apply_one(&mut program, add); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, five)); + + let add = program.new_instruction(Operator::Iadd, Type::i32(), vec![], vec![five, five]); + let replacement = optimizer.apply_one(&mut program, add); + assert!(replacement.is_none()); +} + +#[test] +fn constant() { + let opts; + let mut optimizer = optimizer!(opts, "(=> (iadd $C $x) (iadd_imm $C $x))"); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let zero = program.r#const(Constant::Int(0, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let add = program.new_instruction(Operator::Iadd, Type::i32(), vec![], vec![five, zero]); + + let expected = program.new_instruction( + Operator::IaddImm, + Type::i32(), + vec![Constant::Int(5, BitWidth::ThirtyTwo).into()], + vec![zero], + ); + + let new = optimizer.apply_one(&mut program, add); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, expected)); + + let mul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, zero]); + let add = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![mul, five]); + let replacement = optimizer.apply_one(&mut program, add); + assert!(replacement.is_none()); +} + +#[test] +fn boolean() { + let opts; + let mut optimizer = optimizer!(opts, "(=> (bint true) 1)"); + + let mut program = Program::default(); + let t = program.r#const(Constant::Bool(true, BitWidth::One), BitWidth::One); + let bint = program.new_instruction(Operator::Bint, Type::i1(), vec![], vec![t]); + let one = program.r#const(Constant::Int(1, BitWidth::One), BitWidth::ThirtyTwo); + + let new = optimizer.apply_one(&mut program, bint); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, one)); + + let f = program.r#const(Constant::Bool(false, BitWidth::One), BitWidth::One); + let bint = program.new_instruction(Operator::Bint, Type::i1(), vec![], vec![f]); + let replacement = optimizer.apply_one(&mut program, bint); + assert!(replacement.is_none()); +} + +#[test] +fn condition_codes() { + let opts; + let mut optimizer = optimizer!(opts, "(=> (icmp eq $x $x) true)"); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::One); + let icmp_eq = program.new_instruction( + Operator::Icmp, + Type::b1(), + vec![ConditionCode::Eq.into()], + vec![five, five], + ); + let t = program.r#const(Constant::Bool(true, BitWidth::One), BitWidth::One); + + let new = optimizer.apply_one(&mut program, icmp_eq); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, t)); + + let icmp_ne = program.new_instruction( + Operator::Icmp, + Type::b1(), + vec![ConditionCode::Ne.into()], + vec![five, five], + ); + let replacement = optimizer.apply_one(&mut program, icmp_ne); + assert!(replacement.is_none()); +} + +#[test] +fn is_power_of_two() { + let opts; + let mut optimizer = optimizer!( + opts, + " +(=> (when (imul $x $C) + (is-power-of-two $C)) + (ishl $x $(log2 $C))) +" + ); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let two = program.r#const(Constant::Int(2, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let imul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, two]); + + let one = program.r#const(Constant::Int(1, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let ishl = program.new_instruction(Operator::Ishl, Type::i32(), vec![], vec![five, one]); + + let new = optimizer.apply_one(&mut program, imul); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, ishl)); + + let three = program.r#const(Constant::Int(3, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let imul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, three]); + + let replacement = optimizer.apply_one(&mut program, imul); + assert!(replacement.is_none()); +} + +#[test] +fn bit_width() { + let opts; + let mut optimizer = optimizer!( + opts, + " +(=> (when (imul $C $x) + (bit-width $C 32)) + (imul_imm $C $x)) +" + ); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let two = program.r#const(Constant::Int(2, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let imul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, two]); + + let imul_imm = program.new_instruction( + Operator::ImulImm, + Type::i32(), + vec![Constant::Int(5, BitWidth::ThirtyTwo).into()], + vec![two], + ); + + let new = optimizer.apply_one(&mut program, imul); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, imul_imm)); + + let five = program.r#const(Constant::Int(5, BitWidth::SixtyFour), BitWidth::SixtyFour); + let two = program.r#const(Constant::Int(2, BitWidth::SixtyFour), BitWidth::SixtyFour); + let imul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, two]); + + let replacement = optimizer.apply_one(&mut program, imul); + assert!(replacement.is_none()); +} + +#[test] +fn fits_in_native_word() { + let opts; + let mut optimizer = optimizer!( + opts, + " +(=> (when (imul $C $x) + (fits-in-native-word $C)) + (imul_imm $C $x)) +" + ); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let two = program.r#const(Constant::Int(2, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let imul = program.new_instruction(Operator::Imul, Type::i32(), vec![], vec![five, two]); + + let imul_imm = program.new_instruction( + Operator::ImulImm, + Type::i32(), + vec![Constant::Int(5, BitWidth::ThirtyTwo).into()], + vec![two], + ); + + let new = optimizer.apply_one(&mut program, imul); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, imul_imm)); + + let five = program.r#const(Constant::Int(5, BitWidth::SixtyFour), BitWidth::SixtyFour); + let two = program.r#const(Constant::Int(2, BitWidth::SixtyFour), BitWidth::SixtyFour); + let imul = program.new_instruction(Operator::Imul, Type::i64(), vec![], vec![five, two]); + + let replacement = optimizer.apply_one(&mut program, imul); + assert!(replacement.is_none()); +} + +#[test] +fn unquote_neg() { + let opts; + let mut optimizer = optimizer!( + opts, + " +(=> (isub $x $C) + (iadd_imm $(neg $C) $x)) +" + ); + + let mut program = Program::default(); + let five = program.r#const(Constant::Int(5, BitWidth::SixtyFour), BitWidth::SixtyFour); + let two = program.r#const(Constant::Int(2, BitWidth::SixtyFour), BitWidth::SixtyFour); + let isub = program.new_instruction(Operator::Isub, Type::i64(), vec![], vec![five, two]); + + let iadd_imm = program.new_instruction( + Operator::IaddImm, + Type::i64(), + vec![Constant::Int(-2 as _, BitWidth::SixtyFour).into()], + vec![five], + ); + + let new = optimizer.apply_one(&mut program, isub); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, iadd_imm)); +} + +#[test] +fn subsumption() { + let opts; + let mut optimizer = optimizer!( + opts, + " +;; NB: the following optimizations are ordered from least to most general, so +;; the first applicable optimization should be the one that is applied. + +(=> (iadd (iadd (iadd $w $x) $y) $z) + (iadd (iadd $w $x) (iadd $y $z))) + +(=> (iadd $C1 $C2) + $(iadd $C1 $C2)) + +(=> (iadd $C $x) + (iadd_imm $C $x)) + +(=> (iadd $x $x) + (ishl_imm 1 $x)) +" + ); + + let mut program = Program::default(); + + let w = program.r#const(Constant::Int(11, BitWidth::SixtyFour), BitWidth::SixtyFour); + let x = program.r#const(Constant::Int(22, BitWidth::SixtyFour), BitWidth::SixtyFour); + let y = program.r#const(Constant::Int(33, BitWidth::SixtyFour), BitWidth::SixtyFour); + let z = program.r#const(Constant::Int(44, BitWidth::SixtyFour), BitWidth::SixtyFour); + + log::debug!("(iadd (iadd (iadd w x) y) z) => (iadd (iadd w x) (iadd y z))"); + + let iadd = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![w, x]); + let iadd = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![iadd, y]); + let iadd = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![iadd, z]); + let expected_lhs = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![w, x]); + let expected_rhs = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![y, z]); + let expected = program.new_instruction( + Operator::Iadd, + Type::i64(), + vec![], + vec![expected_lhs, expected_rhs], + ); + + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, expected)); + + log::debug!("(iadd w x) => y"); + + let iadd = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![w, x]); + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, y)); + + log::debug!("(iadd x (iadd y z)) => (iadd_imm x (iadd y z))"); + + let iadd_y_z = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![y, z]); + let iadd = program.new_instruction(Operator::Iadd, Type::i64(), vec![], vec![x, iadd_y_z]); + let iadd_imm = program.new_instruction( + Operator::IaddImm, + Type::i64(), + vec![Constant::Int(22, BitWidth::SixtyFour).into()], + vec![iadd_y_z], + ); + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, iadd_imm)); + + log::debug!("(iadd (imul_imm x 1) (imul_imm x 1)) => (ishl_imm 1 (imul_imm x 1))"); + + let imul_imm = program.new_instruction( + Operator::ImulImm, + Type::i64(), + vec![Constant::Int(1, BitWidth::SixtyFour).into()], + vec![x], + ); + let iadd = program.new_instruction( + Operator::Iadd, + Type::i64(), + vec![], + vec![imul_imm, imul_imm], + ); + let ishl_imm = program.new_instruction( + Operator::IshlImm, + Type::i64(), + vec![Constant::Int(1, BitWidth::SixtyFour).into()], + vec![imul_imm], + ); + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, ishl_imm)); + + log::debug!("(iadd (imul w x) (imul y z)) does not match any optimization."); + + let imul_w_x = program.new_instruction(Operator::Imul, Type::i64(), vec![], vec![w, x]); + let imul_y_z = program.new_instruction(Operator::Imul, Type::i64(), vec![], vec![y, z]); + let iadd = program.new_instruction( + Operator::Iadd, + Type::i64(), + vec![], + vec![imul_w_x, imul_y_z], + ); + + let replacement = optimizer.apply_one(&mut program, iadd); + assert!(replacement.is_none()); +} + +#[test] +fn polymorphic_bit_widths() { + let opts; + let mut optimizer = optimizer!(opts, "(=> (iadd $C $x) (iadd_imm $C $x))"); + + let mut program = Program::default(); + + // Applies to 32 bit adds. + + let x = program.r#const(Constant::Int(42, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let y = program.r#const(Constant::Int(420, BitWidth::ThirtyTwo), BitWidth::ThirtyTwo); + let iadd = program.new_instruction(Operator::Iadd, Type::i32(), vec![], vec![x, y]); + let iadd_imm = program.new_instruction( + Operator::IaddImm, + Type::i32(), + vec![Constant::Int(42, BitWidth::ThirtyTwo).into()], + vec![y], + ); + + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, iadd_imm)); + + // Applies to 16 bit adds. + + let x = program.r#const(Constant::Int(42, BitWidth::Sixteen), BitWidth::Sixteen); + let y = program.r#const(Constant::Int(420, BitWidth::Sixteen), BitWidth::Sixteen); + let iadd = program.new_instruction(Operator::Iadd, Type::i16(), vec![], vec![x, y]); + let iadd_imm = program.new_instruction( + Operator::IaddImm, + Type::i16(), + vec![Constant::Int(42, BitWidth::Sixteen).into()], + vec![y], + ); + + let new = optimizer.apply_one(&mut program, iadd); + let new = new.expect("optimization should have applied"); + assert!(program.structurally_eq(new, iadd_imm)); +}