diff --git a/compiler/noirc_evaluator/src/acir/tests/mod.rs b/compiler/noirc_evaluator/src/acir/tests/mod.rs index 9c2d07b891f..f615029c7ce 100644 --- a/compiler/noirc_evaluator/src/acir/tests/mod.rs +++ b/compiler/noirc_evaluator/src/acir/tests/mod.rs @@ -21,6 +21,7 @@ use crate::{ brillig::{Brillig, BrilligOptions, brillig_ir::artifact::GeneratedBrillig}, ssa::{ function_builder::FunctionBuilder, + interpreter::value::Value, ir::{ function::FunctionId, instruction::BinaryOp, @@ -924,3 +925,208 @@ fn do_not_overflow_with_constant_constrain_neq() { assert_eq!(acir_functions.len(), 1); assert!(acir_functions[0].opcodes().is_empty()); } + +/// Convert the SSA input into ACIR and use ACVM to execute it +/// Returns the ACVM execution status and the value of the 'output' witness value, +/// unless the provided output is None or the ACVM fails during execution. +fn execute_ssa( + ssa: Ssa, + initial_witness: WitnessMap, + output: Option<&Witness>, +) -> (ACVMStatus, Option) { + let brillig = ssa.to_brillig(&BrilligOptions::default()); + let (acir_functions, brillig_functions, _, _) = ssa + .into_acir(&brillig, &BrilligOptions::default(), ExpressionWidth::default()) + .expect("Should compile manually written SSA into ACIR"); + assert_eq!(acir_functions.len(), 1); + let main = &acir_functions[0]; + let mut acvm = ACVM::new( + &StubbedBlackBoxSolver(true), + main.opcodes(), + initial_witness, + &brillig_functions, + &[], + ); + let status = acvm.solve(); + if status == ACVMStatus::Solved { + (status, output.map(|o| acvm.witness_map()[o])) + } else { + (status, None) + } +} + +fn get_main_src(typ: &str) -> String { + format!( + "acir(inline) fn main f0 {{ + b0(inputs: [{typ}; 2]): + lhs = array_get inputs, index u32 0 -> {typ} + rhs = array_get inputs, index u32 1 -> {typ} + " + ) +} + +/// Create a SSA instruction corresponding to the operator, using v1 and v2 as operands. +/// Additional information can be added to the string, +/// for instance, "range_check 8" creates 'range_check v1 to 8 bits' +fn generate_test_instruction_from_operator(operator: &str) -> (String, bool) { + let ops = operator.split(" ").collect::>(); + let op = ops[0]; + let mut output = true; + let src = match op { + "constrain" => { + output = false; + format!("constrain lhs {} rhs", ops[1]) + } + "not" => format!("result = {} lhs", op), + "truncate" => { + format!("result = truncate lhs to {} bits, max_bit_size: {}", ops[1], ops[2]) + } + "range_check" => { + output = false; + format!("range_check lhs to {} bits", ops[1]) + } + _ => format!("result = {} lhs, rhs", op), + }; + + if output { + ( + format!( + " + {src} + return result + }}" + ), + true, + ) + } else { + ( + format!( + " + {src} + return + }}" + ), + false, + ) + } +} + +/// Execute a simple operation for each operators +/// The operation is executed from SSA IR using ACVM after acir-gen +/// and also directly on the SSA IR using the SSA interpreter. +/// The results are compared to ensure that both executions yield the same result. +fn test_operators( + // The list of operators to test + operators: &[&str], + // the type of the input values + typ: &str, + // the values of the inputs + inputs: &[FieldElement], +) { + let main = get_main_src(typ); + let num_type = match typ.chars().next().unwrap() { + 'F' => NumericType::NativeField, + 'i' => NumericType::Signed { bit_size: typ[1..].parse().unwrap() }, + 'u' => NumericType::Unsigned { bit_size: typ[1..].parse().unwrap() }, + _ => unreachable!("invalid numeric type"), + }; + let inputs_int = Value::array_from_iter(inputs.iter().cloned(), num_type).unwrap(); + let inputs = + inputs.iter().enumerate().map(|(i, f)| (Witness(i as u32), *f)).collect::>(); + let len = inputs.len() as u32; + let initial_witness = WitnessMap::from(inputs); + + for op in operators { + let (src, with_output) = generate_test_instruction_from_operator(op); + let output = if with_output { Some(Witness(len)) } else { None }; + let ssa = Ssa::from_str(&(main.to_owned() + &src)).unwrap(); + // ssa execution + let ssa_interpreter_result = ssa.interpret(vec![inputs_int.clone()]); + // acir execution + let acir_execution_result = execute_ssa(ssa, initial_witness.clone(), output.as_ref()); + + match (ssa_interpreter_result, acir_execution_result) { + // Both execution failed, so it is the same behavior, as expected. + (Err(_), (ACVMStatus::Failure(_), _)) => (), + // Both execution succeeded and output the same value + (Ok(ssa_inner_result), (ACVMStatus::Solved, acvm_result)) => { + let ssa_result = if let Some(result) = ssa_inner_result.first() { + result.as_numeric().map(|v| v.convert_to_field()) + } else { + None + }; + assert_eq!(ssa_result, acvm_result); + } + _ => panic!("ssa and acvm execution should have the same result"), + } + } +} + +#[test] +fn test_binary_on_field() { + // Test the following Binary operation on Fields + let operators = [ + "add", + "sub", + "mul", + "div", + "eq", + // Bitwise operations are not allowed on field elements + // SSA interpreter will emit an error but not ACVM + // "and", + // "xor", + "unchecked_add", + "unchecked_sub", + "unchecked_mul", + "range_check 32", + "truncate 32 254", + ]; + let inputs = [FieldElement::from(1_usize), FieldElement::from(2_usize)]; + test_operators(&operators, "Field", &inputs); +} + +#[test] +fn test_u32() { + // Test the following operations on u32 + let operators = [ + "add", + "sub", + "mul", + "div", + "eq", + "and", + "xor", + "unchecked_add", + "unchecked_sub", + "unchecked_mul", + "mod", + "lt", + "or", + "not", + "range_check 8", + "truncate 8 32", + ]; + let inputs = [FieldElement::from(2_usize), FieldElement::from(1_usize)]; + test_operators(&operators, "u32", &inputs); + + // "unchecked_sub" 300 - 500 is not a valid ssa instruction because it assumes no over/under flow. + // it is translated into 300 - 500 in ACVM, which gives -200 + // it is translated into 300 - 500 2**64 in SSA interpreter + let operators = ["unchecked_add", "unchecked_mul", "range_check 8", "truncate 8 32"]; + let inputs = [FieldElement::from(300_usize), FieldElement::from(500_usize)]; + test_operators(&operators, "u32", &inputs); +} + +#[test] +fn test_constraint() { + let operators = ["constrain ==", "constrain !="]; + // Test constraints on Fields with distinct inputs + test_operators( + &operators, + "Field", + &[FieldElement::from(1_usize), FieldElement::from(2_usize)], + ); + + // u32, equal inputs + test_operators(&operators, "u32", &[FieldElement::from(2_usize), FieldElement::from(2_usize)]); +}