Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions compiler/noirc_evaluator/src/acir/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<FieldElement>,
output: Option<&Witness>,
) -> (ACVMStatus<FieldElement>, Option<FieldElement>) {
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::<Vec<_>>();
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::<BTreeMap<_, _>>();
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can follow-up with expected error tests that would have corresponding issues.

"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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in follow-ups we can test the remaining numeric types then.


// "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)]);
}
Loading