diff --git a/compiler/noirc_evaluator/src/ssa/ir/instruction.rs b/compiler/noirc_evaluator/src/ssa/ir/instruction.rs index dbad562b6f4..280a0c32e64 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/instruction.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/instruction.rs @@ -1,6 +1,5 @@ use acvm::{acir::BlackBoxFunc, FieldElement}; use iter_extended::vecmap; -use num_bigint::BigUint; use super::{ basic_block::BasicBlockId, @@ -10,9 +9,15 @@ use super::{ value::{Value, ValueId}, }; +mod binary; mod call; +mod cast; +mod constrain; +pub(crate) use binary::{Binary, BinaryOp}; use call::simplify_call; +use cast::simplify_cast; +use constrain::decompose_constrain; /// Reference to an instruction /// @@ -545,179 +550,6 @@ impl Instruction { } } -/// Try to simplify this cast instruction. If the instruction can be simplified to a known value, -/// that value is returned. Otherwise None is returned. -fn simplify_cast(value: ValueId, dst_typ: &Type, dfg: &mut DataFlowGraph) -> SimplifyResult { - use SimplifyResult::*; - let value = dfg.resolve(value); - - if let Value::Instruction { instruction, .. } = &dfg[value] { - if let Instruction::Cast(original_value, _) = &dfg[*instruction] { - return SimplifiedToInstruction(Instruction::Cast(*original_value, dst_typ.clone())); - } - } - - if let Some(constant) = dfg.get_numeric_constant(value) { - let src_typ = dfg.type_of_value(value); - match (src_typ, dst_typ) { - (Type::Numeric(NumericType::NativeField), Type::Numeric(NumericType::NativeField)) => { - // Field -> Field: use src value - SimplifiedTo(value) - } - ( - Type::Numeric(NumericType::Unsigned { .. }), - Type::Numeric(NumericType::NativeField), - ) => { - // Unsigned -> Field: redefine same constant as Field - SimplifiedTo(dfg.make_constant(constant, dst_typ.clone())) - } - ( - Type::Numeric( - NumericType::NativeField - | NumericType::Unsigned { .. } - | NumericType::Signed { .. }, - ), - Type::Numeric(NumericType::Unsigned { bit_size }), - ) => { - // Field/Unsigned -> unsigned: truncate - let integer_modulus = BigUint::from(2u128).pow(*bit_size); - let constant: BigUint = BigUint::from_bytes_be(&constant.to_be_bytes()); - let truncated = constant % integer_modulus; - let truncated = FieldElement::from_be_bytes_reduce(&truncated.to_bytes_be()); - SimplifiedTo(dfg.make_constant(truncated, dst_typ.clone())) - } - _ => None, - } - } else if *dst_typ == dfg.type_of_value(value) { - SimplifiedTo(value) - } else { - None - } -} - -/// Try to decompose this constrain instruction. This constraint will be broken down such that it instead constrains -/// all the values which are used to compute the values which were being constrained. -fn decompose_constrain( - lhs: ValueId, - rhs: ValueId, - msg: Option, - dfg: &mut DataFlowGraph, -) -> Vec { - let lhs = dfg.resolve(lhs); - let rhs = dfg.resolve(rhs); - - if lhs == rhs { - // Remove trivial case `assert_eq(x, x)` - Vec::new() - } else { - match (&dfg[lhs], &dfg[rhs]) { - (Value::NumericConstant { constant, typ }, Value::Instruction { instruction, .. }) - | (Value::Instruction { instruction, .. }, Value::NumericConstant { constant, typ }) - if *typ == Type::bool() => - { - match dfg[*instruction] { - Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Eq }) - if constant.is_one() => - { - // Replace an explicit two step equality assertion - // - // v2 = eq v0, u32 v1 - // constrain v2 == u1 1 - // - // with a direct assertion of equality between the two values - // - // v2 = eq v0, u32 v1 - // constrain v0 == v1 - // - // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it - // will likely be removed through dead instruction elimination. - - vec![Instruction::Constrain(lhs, rhs, msg)] - } - - Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Mul }) - if constant.is_one() && dfg.type_of_value(lhs) == Type::bool() => - { - // Replace an equality assertion on a boolean multiplication - // - // v2 = mul v0, v1 - // constrain v2 == u1 1 - // - // with a direct assertion that each value is equal to 1 - // - // v2 = mul v0, v1 - // constrain v0 == 1 - // constrain v1 == 1 - // - // This is due to the fact that for `v2` to be 1 then both `v0` and `v1` are 1. - // - // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it - // will likely be removed through dead instruction elimination. - let one = FieldElement::one(); - let one = dfg.make_constant(one, Type::bool()); - - [ - decompose_constrain(lhs, one, msg.clone(), dfg), - decompose_constrain(rhs, one, msg, dfg), - ] - .concat() - } - - Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Or }) - if constant.is_zero() => - { - // Replace an equality assertion on an OR - // - // v2 = or v0, v1 - // constrain v2 == u1 0 - // - // with a direct assertion that each value is equal to 0 - // - // v2 = or v0, v1 - // constrain v0 == 0 - // constrain v1 == 0 - // - // This is due to the fact that for `v2` to be 0 then both `v0` and `v1` are 0. - // - // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it - // will likely be removed through dead instruction elimination. - let zero = FieldElement::zero(); - let zero = dfg.make_constant(zero, dfg.type_of_value(lhs)); - - [ - decompose_constrain(lhs, zero, msg.clone(), dfg), - decompose_constrain(rhs, zero, msg, dfg), - ] - .concat() - } - - Instruction::Not(value) => { - // Replace an assertion that a not instruction is truthy - // - // v1 = not v0 - // constrain v1 == u1 1 - // - // with an assertion that the not instruction input is falsy - // - // v1 = not v0 - // constrain v0 == u1 0 - // - // Note that this doesn't remove the value `v1` as it may be used in other instructions, but it - // will likely be removed through dead instruction elimination. - let reversed_constant = FieldElement::from(!constant.is_one()); - let reversed_constant = dfg.make_constant(reversed_constant, Type::bool()); - decompose_constrain(value, reversed_constant, msg, dfg) - } - - _ => vec![Instruction::Constrain(lhs, rhs, msg)], - } - } - - _ => vec![Instruction::Constrain(lhs, rhs, msg)], - } - } -} - /// The possible return values for Instruction::return_types pub(crate) enum InstructionResultType { /// The result type of this instruction matches that of this operand @@ -848,350 +680,6 @@ impl TerminatorInstruction { } } -/// A binary instruction in the IR. -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub(crate) struct Binary { - /// Left hand side of the binary operation - pub(crate) lhs: ValueId, - /// Right hand side of the binary operation - pub(crate) rhs: ValueId, - /// The binary operation to apply - pub(crate) operator: BinaryOp, -} - -impl Binary { - /// The type of this Binary instruction's result - pub(crate) fn result_type(&self) -> InstructionResultType { - match self.operator { - BinaryOp::Eq | BinaryOp::Lt => InstructionResultType::Known(Type::bool()), - _ => InstructionResultType::Operand(self.lhs), - } - } - - /// Try to simplify this binary instruction, returning the new value if possible. - fn simplify(&self, dfg: &mut DataFlowGraph) -> SimplifyResult { - let lhs = dfg.get_numeric_constant(self.lhs); - let rhs = dfg.get_numeric_constant(self.rhs); - let operand_type = dfg.type_of_value(self.lhs); - - if let (Some(lhs), Some(rhs)) = (lhs, rhs) { - return match eval_constant_binary_op(lhs, rhs, self.operator, operand_type) { - Some((result, result_type)) => { - let value = dfg.make_constant(result, result_type); - SimplifyResult::SimplifiedTo(value) - } - None => SimplifyResult::None, - }; - } - - let lhs_is_zero = lhs.map_or(false, |lhs| lhs.is_zero()); - let rhs_is_zero = rhs.map_or(false, |rhs| rhs.is_zero()); - - let lhs_is_one = lhs.map_or(false, |lhs| lhs.is_one()); - let rhs_is_one = rhs.map_or(false, |rhs| rhs.is_one()); - - match self.operator { - BinaryOp::Add => { - if lhs_is_zero { - return SimplifyResult::SimplifiedTo(self.rhs); - } - if rhs_is_zero { - return SimplifyResult::SimplifiedTo(self.lhs); - } - } - BinaryOp::Sub => { - if rhs_is_zero { - return SimplifyResult::SimplifiedTo(self.lhs); - } - } - BinaryOp::Mul => { - if lhs_is_one { - return SimplifyResult::SimplifiedTo(self.rhs); - } - if rhs_is_one { - return SimplifyResult::SimplifiedTo(self.lhs); - } - if lhs_is_zero || rhs_is_zero { - let zero = dfg.make_constant(FieldElement::zero(), operand_type); - return SimplifyResult::SimplifiedTo(zero); - } - } - BinaryOp::Div => { - if rhs_is_one { - return SimplifyResult::SimplifiedTo(self.lhs); - } - } - BinaryOp::Mod => { - if rhs_is_one { - let zero = dfg.make_constant(FieldElement::zero(), operand_type); - return SimplifyResult::SimplifiedTo(zero); - } - } - BinaryOp::Eq => { - if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { - let one = dfg.make_constant(FieldElement::one(), Type::bool()); - return SimplifyResult::SimplifiedTo(one); - } - if operand_type == Type::bool() { - // Simplify forms of `(boolean == true)` into `boolean` - if lhs_is_one { - return SimplifyResult::SimplifiedTo(self.rhs); - } - if rhs_is_one { - return SimplifyResult::SimplifiedTo(self.lhs); - } - // Simplify forms of `(boolean == false)` into `!boolean` - if lhs_is_zero { - return SimplifyResult::SimplifiedToInstruction(Instruction::Not(self.rhs)); - } - if rhs_is_zero { - return SimplifyResult::SimplifiedToInstruction(Instruction::Not(self.lhs)); - } - } - } - BinaryOp::Lt => { - if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { - let zero = dfg.make_constant(FieldElement::zero(), Type::bool()); - return SimplifyResult::SimplifiedTo(zero); - } - if operand_type.is_unsigned() { - if rhs_is_zero { - // Unsigned values cannot be less than zero. - let zero = dfg.make_constant(FieldElement::zero(), Type::bool()); - return SimplifyResult::SimplifiedTo(zero); - } else if rhs_is_one { - let zero = dfg.make_constant(FieldElement::zero(), operand_type); - return SimplifyResult::SimplifiedToInstruction(Instruction::binary( - BinaryOp::Eq, - self.lhs, - zero, - )); - } - } - } - BinaryOp::And => { - if lhs_is_zero || rhs_is_zero { - let zero = dfg.make_constant(FieldElement::zero(), operand_type); - return SimplifyResult::SimplifiedTo(zero); - } - if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { - return SimplifyResult::SimplifiedTo(self.lhs); - } - if operand_type == Type::bool() { - // Boolean AND is equivalent to multiplication, which is a cheaper operation. - let instruction = Instruction::binary(BinaryOp::Mul, self.lhs, self.rhs); - return SimplifyResult::SimplifiedToInstruction(instruction); - } - } - BinaryOp::Or => { - if lhs_is_zero { - return SimplifyResult::SimplifiedTo(self.rhs); - } - if rhs_is_zero { - return SimplifyResult::SimplifiedTo(self.lhs); - } - if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { - return SimplifyResult::SimplifiedTo(self.lhs); - } - } - BinaryOp::Xor => { - if lhs_is_zero { - return SimplifyResult::SimplifiedTo(self.rhs); - } - if rhs_is_zero { - return SimplifyResult::SimplifiedTo(self.lhs); - } - if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { - let zero = dfg.make_constant(FieldElement::zero(), operand_type); - return SimplifyResult::SimplifiedTo(zero); - } - } - } - SimplifyResult::None - } -} - -/// Evaluate a binary operation with constant arguments. -fn eval_constant_binary_op( - lhs: FieldElement, - rhs: FieldElement, - operator: BinaryOp, - mut operand_type: Type, -) -> Option<(FieldElement, Type)> { - let value = match &operand_type { - Type::Numeric(NumericType::NativeField) => { - // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. - // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, - // and the operation should be handled by ACIR generation. - if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == FieldElement::zero() { - return None; - } - operator.get_field_function()?(lhs, rhs) - } - Type::Numeric(NumericType::Unsigned { bit_size }) => { - let function = operator.get_u128_function(); - - let lhs = truncate(lhs.try_into_u128()?, *bit_size); - let rhs = truncate(rhs.try_into_u128()?, *bit_size); - - // The divisor is being truncated into the type of the operand, which can potentially - // lead to the rhs being zero. - // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. - // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, - // and the operation should be handled by ACIR generation. - if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == 0 { - return None; - } - let result = function(lhs, rhs)?; - // Check for overflow - if result >= 2u128.pow(*bit_size) { - return None; - } - result.into() - } - Type::Numeric(NumericType::Signed { bit_size }) => { - let function = operator.get_i128_function(); - - let lhs = truncate(lhs.try_into_u128()?, *bit_size); - let rhs = truncate(rhs.try_into_u128()?, *bit_size); - let l_pos = lhs < 2u128.pow(bit_size - 1); - let r_pos = rhs < 2u128.pow(bit_size - 1); - let lhs = if l_pos { lhs as i128 } else { -((2u128.pow(*bit_size) - lhs) as i128) }; - let rhs = if r_pos { rhs as i128 } else { -((2u128.pow(*bit_size) - rhs) as i128) }; - // The divisor is being truncated into the type of the operand, which can potentially - // lead to the rhs being zero. - // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. - // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, - // and the operation should be handled by ACIR generation. - if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == 0 { - return None; - } - - let result = function(lhs, rhs)?; - // Check for overflow - if result >= 2i128.pow(*bit_size - 1) || result < -(2i128.pow(*bit_size - 1)) { - return None; - } - let result = - if result >= 0 { result as u128 } else { (2i128.pow(*bit_size) + result) as u128 }; - result.into() - } - _ => return None, - }; - - if matches!(operator, BinaryOp::Eq | BinaryOp::Lt) { - operand_type = Type::bool(); - } - - Some((value, operand_type)) -} - -fn truncate(int: u128, bit_size: u32) -> u128 { - let max = 2u128.pow(bit_size); - int % max -} - -impl BinaryOp { - fn get_field_function(self) -> Option FieldElement> { - match self { - BinaryOp::Add => Some(std::ops::Add::add), - BinaryOp::Sub => Some(std::ops::Sub::sub), - BinaryOp::Mul => Some(std::ops::Mul::mul), - BinaryOp::Div => Some(std::ops::Div::div), - BinaryOp::Eq => Some(|x, y| (x == y).into()), - BinaryOp::Lt => Some(|x, y| (x < y).into()), - // Bitwise operators are unsupported for Fields - BinaryOp::Mod => None, - BinaryOp::And => None, - BinaryOp::Or => None, - BinaryOp::Xor => None, - } - } - - fn get_u128_function(self) -> fn(u128, u128) -> Option { - match self { - BinaryOp::Add => u128::checked_add, - BinaryOp::Sub => u128::checked_sub, - BinaryOp::Mul => u128::checked_mul, - BinaryOp::Div => u128::checked_div, - BinaryOp::Mod => u128::checked_rem, - BinaryOp::And => |x, y| Some(x & y), - BinaryOp::Or => |x, y| Some(x | y), - BinaryOp::Xor => |x, y| Some(x ^ y), - BinaryOp::Eq => |x, y| Some((x == y) as u128), - BinaryOp::Lt => |x, y| Some((x < y) as u128), - } - } - - fn get_i128_function(self) -> fn(i128, i128) -> Option { - match self { - BinaryOp::Add => i128::checked_add, - BinaryOp::Sub => i128::checked_sub, - BinaryOp::Mul => i128::checked_mul, - BinaryOp::Div => i128::checked_div, - BinaryOp::Mod => i128::checked_rem, - BinaryOp::And => |x, y| Some(x & y), - BinaryOp::Or => |x, y| Some(x | y), - BinaryOp::Xor => |x, y| Some(x ^ y), - BinaryOp::Eq => |x, y| Some((x == y) as i128), - BinaryOp::Lt => |x, y| Some((x < y) as i128), - } - } -} - -/// Binary Operations allowed in the IR. -/// Aside from the comparison operators (Eq and Lt), all operators -/// will return the same type as their operands. -/// The operand types must match for all binary operators. -/// All binary operators are also only for numeric types. To implement -/// e.g. equality for a compound type like a struct, one must add a -/// separate Eq operation for each field and combine them later with And. -#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] -pub(crate) enum BinaryOp { - /// Addition of lhs + rhs. - Add, - /// Subtraction of lhs - rhs. - Sub, - /// Multiplication of lhs * rhs. - Mul, - /// Division of lhs / rhs. - Div, - /// Modulus of lhs % rhs. - Mod, - /// Checks whether two types are equal. - /// Returns true if the types were equal and - /// false otherwise. - Eq, - /// Checks whether the lhs is less than the rhs. - /// All other comparison operators should be translated - /// to less than. For example (a > b) = (b < a) = !(a >= b) = !(b <= a). - /// The result will always be a u1. - Lt, - /// Bitwise and (&) - And, - /// Bitwise or (|) - Or, - /// Bitwise xor (^) - Xor, -} - -impl std::fmt::Display for BinaryOp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BinaryOp::Add => write!(f, "add"), - BinaryOp::Sub => write!(f, "sub"), - BinaryOp::Mul => write!(f, "mul"), - BinaryOp::Div => write!(f, "div"), - BinaryOp::Eq => write!(f, "eq"), - BinaryOp::Mod => write!(f, "mod"), - BinaryOp::Lt => write!(f, "lt"), - BinaryOp::And => write!(f, "and"), - BinaryOp::Or => write!(f, "or"), - BinaryOp::Xor => write!(f, "xor"), - } - } -} - /// Contains the result to Instruction::simplify, specifying how the instruction /// should be simplified. pub(crate) enum SimplifyResult { diff --git a/compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs b/compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs new file mode 100644 index 00000000000..1cb32d94148 --- /dev/null +++ b/compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs @@ -0,0 +1,349 @@ +use acvm::FieldElement; + +use super::{ + DataFlowGraph, Instruction, InstructionResultType, NumericType, SimplifyResult, Type, ValueId, +}; + +/// Binary Operations allowed in the IR. +/// Aside from the comparison operators (Eq and Lt), all operators +/// will return the same type as their operands. +/// The operand types must match for all binary operators. +/// All binary operators are also only for numeric types. To implement +/// e.g. equality for a compound type like a struct, one must add a +/// separate Eq operation for each field and combine them later with And. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub(crate) enum BinaryOp { + /// Addition of lhs + rhs. + Add, + /// Subtraction of lhs - rhs. + Sub, + /// Multiplication of lhs * rhs. + Mul, + /// Division of lhs / rhs. + Div, + /// Modulus of lhs % rhs. + Mod, + /// Checks whether two types are equal. + /// Returns true if the types were equal and + /// false otherwise. + Eq, + /// Checks whether the lhs is less than the rhs. + /// All other comparison operators should be translated + /// to less than. For example (a > b) = (b < a) = !(a >= b) = !(b <= a). + /// The result will always be a u1. + Lt, + /// Bitwise and (&) + And, + /// Bitwise or (|) + Or, + /// Bitwise xor (^) + Xor, +} + +impl std::fmt::Display for BinaryOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BinaryOp::Add => write!(f, "add"), + BinaryOp::Sub => write!(f, "sub"), + BinaryOp::Mul => write!(f, "mul"), + BinaryOp::Div => write!(f, "div"), + BinaryOp::Eq => write!(f, "eq"), + BinaryOp::Mod => write!(f, "mod"), + BinaryOp::Lt => write!(f, "lt"), + BinaryOp::And => write!(f, "and"), + BinaryOp::Or => write!(f, "or"), + BinaryOp::Xor => write!(f, "xor"), + } + } +} + +/// A binary instruction in the IR. +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) struct Binary { + /// Left hand side of the binary operation + pub(crate) lhs: ValueId, + /// Right hand side of the binary operation + pub(crate) rhs: ValueId, + /// The binary operation to apply + pub(crate) operator: BinaryOp, +} + +impl Binary { + /// The type of this Binary instruction's result + pub(crate) fn result_type(&self) -> InstructionResultType { + match self.operator { + BinaryOp::Eq | BinaryOp::Lt => InstructionResultType::Known(Type::bool()), + _ => InstructionResultType::Operand(self.lhs), + } + } + + /// Try to simplify this binary instruction, returning the new value if possible. + pub(super) fn simplify(&self, dfg: &mut DataFlowGraph) -> SimplifyResult { + let lhs = dfg.get_numeric_constant(self.lhs); + let rhs = dfg.get_numeric_constant(self.rhs); + let operand_type = dfg.type_of_value(self.lhs); + + if let (Some(lhs), Some(rhs)) = (lhs, rhs) { + return match eval_constant_binary_op(lhs, rhs, self.operator, operand_type) { + Some((result, result_type)) => { + let value = dfg.make_constant(result, result_type); + SimplifyResult::SimplifiedTo(value) + } + None => SimplifyResult::None, + }; + } + + let lhs_is_zero = lhs.map_or(false, |lhs| lhs.is_zero()); + let rhs_is_zero = rhs.map_or(false, |rhs| rhs.is_zero()); + + let lhs_is_one = lhs.map_or(false, |lhs| lhs.is_one()); + let rhs_is_one = rhs.map_or(false, |rhs| rhs.is_one()); + + match self.operator { + BinaryOp::Add => { + if lhs_is_zero { + return SimplifyResult::SimplifiedTo(self.rhs); + } + if rhs_is_zero { + return SimplifyResult::SimplifiedTo(self.lhs); + } + } + BinaryOp::Sub => { + if rhs_is_zero { + return SimplifyResult::SimplifiedTo(self.lhs); + } + } + BinaryOp::Mul => { + if lhs_is_one { + return SimplifyResult::SimplifiedTo(self.rhs); + } + if rhs_is_one { + return SimplifyResult::SimplifiedTo(self.lhs); + } + if lhs_is_zero || rhs_is_zero { + let zero = dfg.make_constant(FieldElement::zero(), operand_type); + return SimplifyResult::SimplifiedTo(zero); + } + } + BinaryOp::Div => { + if rhs_is_one { + return SimplifyResult::SimplifiedTo(self.lhs); + } + } + BinaryOp::Mod => { + if rhs_is_one { + let zero = dfg.make_constant(FieldElement::zero(), operand_type); + return SimplifyResult::SimplifiedTo(zero); + } + } + BinaryOp::Eq => { + if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { + let one = dfg.make_constant(FieldElement::one(), Type::bool()); + return SimplifyResult::SimplifiedTo(one); + } + if operand_type == Type::bool() { + // Simplify forms of `(boolean == true)` into `boolean` + if lhs_is_one { + return SimplifyResult::SimplifiedTo(self.rhs); + } + if rhs_is_one { + return SimplifyResult::SimplifiedTo(self.lhs); + } + // Simplify forms of `(boolean == false)` into `!boolean` + if lhs_is_zero { + return SimplifyResult::SimplifiedToInstruction(Instruction::Not(self.rhs)); + } + if rhs_is_zero { + return SimplifyResult::SimplifiedToInstruction(Instruction::Not(self.lhs)); + } + } + } + BinaryOp::Lt => { + if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { + let zero = dfg.make_constant(FieldElement::zero(), Type::bool()); + return SimplifyResult::SimplifiedTo(zero); + } + if operand_type.is_unsigned() { + if rhs_is_zero { + // Unsigned values cannot be less than zero. + let zero = dfg.make_constant(FieldElement::zero(), Type::bool()); + return SimplifyResult::SimplifiedTo(zero); + } else if rhs_is_one { + let zero = dfg.make_constant(FieldElement::zero(), operand_type); + return SimplifyResult::SimplifiedToInstruction(Instruction::binary( + BinaryOp::Eq, + self.lhs, + zero, + )); + } + } + } + BinaryOp::And => { + if lhs_is_zero || rhs_is_zero { + let zero = dfg.make_constant(FieldElement::zero(), operand_type); + return SimplifyResult::SimplifiedTo(zero); + } + if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { + return SimplifyResult::SimplifiedTo(self.lhs); + } + if operand_type == Type::bool() { + // Boolean AND is equivalent to multiplication, which is a cheaper operation. + let instruction = Instruction::binary(BinaryOp::Mul, self.lhs, self.rhs); + return SimplifyResult::SimplifiedToInstruction(instruction); + } + } + BinaryOp::Or => { + if lhs_is_zero { + return SimplifyResult::SimplifiedTo(self.rhs); + } + if rhs_is_zero { + return SimplifyResult::SimplifiedTo(self.lhs); + } + if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { + return SimplifyResult::SimplifiedTo(self.lhs); + } + } + BinaryOp::Xor => { + if lhs_is_zero { + return SimplifyResult::SimplifiedTo(self.rhs); + } + if rhs_is_zero { + return SimplifyResult::SimplifiedTo(self.lhs); + } + if dfg.resolve(self.lhs) == dfg.resolve(self.rhs) { + let zero = dfg.make_constant(FieldElement::zero(), operand_type); + return SimplifyResult::SimplifiedTo(zero); + } + } + } + SimplifyResult::None + } +} + +/// Evaluate a binary operation with constant arguments. +fn eval_constant_binary_op( + lhs: FieldElement, + rhs: FieldElement, + operator: BinaryOp, + mut operand_type: Type, +) -> Option<(FieldElement, Type)> { + let value = match &operand_type { + Type::Numeric(NumericType::NativeField) => { + // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. + // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, + // and the operation should be handled by ACIR generation. + if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == FieldElement::zero() { + return None; + } + operator.get_field_function()?(lhs, rhs) + } + Type::Numeric(NumericType::Unsigned { bit_size }) => { + let function = operator.get_u128_function(); + + let lhs = truncate(lhs.try_into_u128()?, *bit_size); + let rhs = truncate(rhs.try_into_u128()?, *bit_size); + + // The divisor is being truncated into the type of the operand, which can potentially + // lead to the rhs being zero. + // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. + // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, + // and the operation should be handled by ACIR generation. + if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == 0 { + return None; + } + let result = function(lhs, rhs)?; + // Check for overflow + if result >= 2u128.pow(*bit_size) { + return None; + } + result.into() + } + Type::Numeric(NumericType::Signed { bit_size }) => { + let function = operator.get_i128_function(); + + let lhs = truncate(lhs.try_into_u128()?, *bit_size); + let rhs = truncate(rhs.try_into_u128()?, *bit_size); + let l_pos = lhs < 2u128.pow(bit_size - 1); + let r_pos = rhs < 2u128.pow(bit_size - 1); + let lhs = if l_pos { lhs as i128 } else { -((2u128.pow(*bit_size) - lhs) as i128) }; + let rhs = if r_pos { rhs as i128 } else { -((2u128.pow(*bit_size) - rhs) as i128) }; + // The divisor is being truncated into the type of the operand, which can potentially + // lead to the rhs being zero. + // If the rhs of a division is zero, attempting to evaluate the division will cause a compiler panic. + // Thus, we do not evaluate the division in this method, as we want to avoid triggering a panic, + // and the operation should be handled by ACIR generation. + if matches!(operator, BinaryOp::Div | BinaryOp::Mod) && rhs == 0 { + return None; + } + + let result = function(lhs, rhs)?; + // Check for overflow + if result >= 2i128.pow(*bit_size - 1) || result < -(2i128.pow(*bit_size - 1)) { + return None; + } + let result = + if result >= 0 { result as u128 } else { (2i128.pow(*bit_size) + result) as u128 }; + result.into() + } + _ => return None, + }; + + if matches!(operator, BinaryOp::Eq | BinaryOp::Lt) { + operand_type = Type::bool(); + } + + Some((value, operand_type)) +} + +fn truncate(int: u128, bit_size: u32) -> u128 { + let max = 2u128.pow(bit_size); + int % max +} + +impl BinaryOp { + fn get_field_function(self) -> Option FieldElement> { + match self { + BinaryOp::Add => Some(std::ops::Add::add), + BinaryOp::Sub => Some(std::ops::Sub::sub), + BinaryOp::Mul => Some(std::ops::Mul::mul), + BinaryOp::Div => Some(std::ops::Div::div), + BinaryOp::Eq => Some(|x, y| (x == y).into()), + BinaryOp::Lt => Some(|x, y| (x < y).into()), + // Bitwise operators are unsupported for Fields + BinaryOp::Mod => None, + BinaryOp::And => None, + BinaryOp::Or => None, + BinaryOp::Xor => None, + } + } + + fn get_u128_function(self) -> fn(u128, u128) -> Option { + match self { + BinaryOp::Add => u128::checked_add, + BinaryOp::Sub => u128::checked_sub, + BinaryOp::Mul => u128::checked_mul, + BinaryOp::Div => u128::checked_div, + BinaryOp::Mod => u128::checked_rem, + BinaryOp::And => |x, y| Some(x & y), + BinaryOp::Or => |x, y| Some(x | y), + BinaryOp::Xor => |x, y| Some(x ^ y), + BinaryOp::Eq => |x, y| Some((x == y) as u128), + BinaryOp::Lt => |x, y| Some((x < y) as u128), + } + } + + fn get_i128_function(self) -> fn(i128, i128) -> Option { + match self { + BinaryOp::Add => i128::checked_add, + BinaryOp::Sub => i128::checked_sub, + BinaryOp::Mul => i128::checked_mul, + BinaryOp::Div => i128::checked_div, + BinaryOp::Mod => i128::checked_rem, + BinaryOp::And => |x, y| Some(x & y), + BinaryOp::Or => |x, y| Some(x | y), + BinaryOp::Xor => |x, y| Some(x ^ y), + BinaryOp::Eq => |x, y| Some((x == y) as i128), + BinaryOp::Lt => |x, y| Some((x < y) as i128), + } + } +} diff --git a/compiler/noirc_evaluator/src/ssa/ir/instruction/cast.rs b/compiler/noirc_evaluator/src/ssa/ir/instruction/cast.rs new file mode 100644 index 00000000000..671820e801d --- /dev/null +++ b/compiler/noirc_evaluator/src/ssa/ir/instruction/cast.rs @@ -0,0 +1,58 @@ +use acvm::FieldElement; +use num_bigint::BigUint; + +use super::{DataFlowGraph, Instruction, NumericType, SimplifyResult, Type, Value, ValueId}; + +/// Try to simplify this cast instruction. If the instruction can be simplified to a known value, +/// that value is returned. Otherwise None is returned. +pub(super) fn simplify_cast( + value: ValueId, + dst_typ: &Type, + dfg: &mut DataFlowGraph, +) -> SimplifyResult { + use SimplifyResult::*; + let value = dfg.resolve(value); + + if let Value::Instruction { instruction, .. } = &dfg[value] { + if let Instruction::Cast(original_value, _) = &dfg[*instruction] { + return SimplifiedToInstruction(Instruction::Cast(*original_value, dst_typ.clone())); + } + } + + if let Some(constant) = dfg.get_numeric_constant(value) { + let src_typ = dfg.type_of_value(value); + match (src_typ, dst_typ) { + (Type::Numeric(NumericType::NativeField), Type::Numeric(NumericType::NativeField)) => { + // Field -> Field: use src value + SimplifiedTo(value) + } + ( + Type::Numeric(NumericType::Unsigned { .. }), + Type::Numeric(NumericType::NativeField), + ) => { + // Unsigned -> Field: redefine same constant as Field + SimplifiedTo(dfg.make_constant(constant, dst_typ.clone())) + } + ( + Type::Numeric( + NumericType::NativeField + | NumericType::Unsigned { .. } + | NumericType::Signed { .. }, + ), + Type::Numeric(NumericType::Unsigned { bit_size }), + ) => { + // Field/Unsigned -> unsigned: truncate + let integer_modulus = BigUint::from(2u128).pow(*bit_size); + let constant: BigUint = BigUint::from_bytes_be(&constant.to_be_bytes()); + let truncated = constant % integer_modulus; + let truncated = FieldElement::from_be_bytes_reduce(&truncated.to_bytes_be()); + SimplifiedTo(dfg.make_constant(truncated, dst_typ.clone())) + } + _ => None, + } + } else if *dst_typ == dfg.type_of_value(value) { + SimplifiedTo(value) + } else { + None + } +} diff --git a/compiler/noirc_evaluator/src/ssa/ir/instruction/constrain.rs b/compiler/noirc_evaluator/src/ssa/ir/instruction/constrain.rs new file mode 100644 index 00000000000..7fb0970c834 --- /dev/null +++ b/compiler/noirc_evaluator/src/ssa/ir/instruction/constrain.rs @@ -0,0 +1,126 @@ +use acvm::FieldElement; + +use super::{Binary, BinaryOp, DataFlowGraph, Instruction, Type, Value, ValueId}; + +/// Try to decompose this constrain instruction. This constraint will be broken down such that it instead constrains +/// all the values which are used to compute the values which were being constrained. +pub(super) fn decompose_constrain( + lhs: ValueId, + rhs: ValueId, + msg: Option, + dfg: &mut DataFlowGraph, +) -> Vec { + let lhs = dfg.resolve(lhs); + let rhs = dfg.resolve(rhs); + + if lhs == rhs { + // Remove trivial case `assert_eq(x, x)` + Vec::new() + } else { + match (&dfg[lhs], &dfg[rhs]) { + (Value::NumericConstant { constant, typ }, Value::Instruction { instruction, .. }) + | (Value::Instruction { instruction, .. }, Value::NumericConstant { constant, typ }) + if *typ == Type::bool() => + { + match dfg[*instruction] { + Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Eq }) + if constant.is_one() => + { + // Replace an explicit two step equality assertion + // + // v2 = eq v0, u32 v1 + // constrain v2 == u1 1 + // + // with a direct assertion of equality between the two values + // + // v2 = eq v0, u32 v1 + // constrain v0 == v1 + // + // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it + // will likely be removed through dead instruction elimination. + + vec![Instruction::Constrain(lhs, rhs, msg)] + } + + Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Mul }) + if constant.is_one() && dfg.type_of_value(lhs) == Type::bool() => + { + // Replace an equality assertion on a boolean multiplication + // + // v2 = mul v0, v1 + // constrain v2 == u1 1 + // + // with a direct assertion that each value is equal to 1 + // + // v2 = mul v0, v1 + // constrain v0 == 1 + // constrain v1 == 1 + // + // This is due to the fact that for `v2` to be 1 then both `v0` and `v1` are 1. + // + // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it + // will likely be removed through dead instruction elimination. + let one = FieldElement::one(); + let one = dfg.make_constant(one, Type::bool()); + + [ + decompose_constrain(lhs, one, msg.clone(), dfg), + decompose_constrain(rhs, one, msg, dfg), + ] + .concat() + } + + Instruction::Binary(Binary { lhs, rhs, operator: BinaryOp::Or }) + if constant.is_zero() => + { + // Replace an equality assertion on an OR + // + // v2 = or v0, v1 + // constrain v2 == u1 0 + // + // with a direct assertion that each value is equal to 0 + // + // v2 = or v0, v1 + // constrain v0 == 0 + // constrain v1 == 0 + // + // This is due to the fact that for `v2` to be 0 then both `v0` and `v1` are 0. + // + // Note that this doesn't remove the value `v2` as it may be used in other instructions, but it + // will likely be removed through dead instruction elimination. + let zero = FieldElement::zero(); + let zero = dfg.make_constant(zero, dfg.type_of_value(lhs)); + + [ + decompose_constrain(lhs, zero, msg.clone(), dfg), + decompose_constrain(rhs, zero, msg, dfg), + ] + .concat() + } + + Instruction::Not(value) => { + // Replace an assertion that a not instruction is truthy + // + // v1 = not v0 + // constrain v1 == u1 1 + // + // with an assertion that the not instruction input is falsy + // + // v1 = not v0 + // constrain v0 == u1 0 + // + // Note that this doesn't remove the value `v1` as it may be used in other instructions, but it + // will likely be removed through dead instruction elimination. + let reversed_constant = FieldElement::from(!constant.is_one()); + let reversed_constant = dfg.make_constant(reversed_constant, Type::bool()); + decompose_constrain(value, reversed_constant, msg, dfg) + } + + _ => vec![Instruction::Constrain(lhs, rhs, msg)], + } + } + + _ => vec![Instruction::Constrain(lhs, rhs, msg)], + } + } +}