diff --git a/compiler/noirc_evaluator/src/acir/tests/brillig_call.rs b/compiler/noirc_evaluator/src/acir/tests/brillig_call.rs new file mode 100644 index 00000000000..a9d89570caf --- /dev/null +++ b/compiler/noirc_evaluator/src/acir/tests/brillig_call.rs @@ -0,0 +1,306 @@ +use acvm::{acir::circuit::brillig::BrilligFunctionId, assert_circuit_snapshot}; + +use crate::acir::tests::ssa_to_acir_program_with_debug_info; + +// Test that given multiple calls to the same brillig function we generate only one bytecode +// and the appropriate Brillig call opcodes are generated +#[test] +fn multiple_brillig_calls_one_bytecode() { + let src = " + acir(inline) fn main f0 { + b0(v0: Field, v1: Field): + v4 = call f1(v0, v1) -> Field + v5 = call f1(v0, v1) -> Field + v6 = call f1(v0, v1) -> Field + v7 = call f2(v0, v1) -> Field + v8 = call f1(v0, v1) -> Field + v9 = call f2(v0, v1) -> Field + return + } + brillig(inline) fn foo f1 { + b0(v0: Field, v1: Field): + v2 = eq v0, v1 + constrain v2 == u1 0 + return v0 + } + brillig(inline) fn foo f2 { + b0(v0: Field, v1: Field): + v2 = eq v0, v1 + constrain v2 == u1 0 + return v0 + } + "; + let (program, debug) = ssa_to_acir_program_with_debug_info(src); + + let main_debug = &debug[0]; + // We have two normal Brillig functions that were called multiple times. + // We should have a single locations map for each function's debug metadata. + assert_eq!(main_debug.brillig_locations.len(), 2); + assert!(main_debug.brillig_locations.contains_key(&BrilligFunctionId(0))); + assert!(main_debug.brillig_locations.contains_key(&BrilligFunctionId(1))); + + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w7 + private parameters: [w0, w1] + public parameters: [] + return values: [] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w2] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w3] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w4] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w5] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w6] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w7] + + unconstrained func 0 + [Const { destination: Direct(2), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Direct(0), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 2 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(32836), size_address: Relative(3), offset_address: Relative(4) }, Mov { destination: Relative(1), source: Direct(32836) }, Mov { destination: Relative(2), source: Direct(32837) }, Call { location: 14 }, Call { location: 15 }, Mov { destination: Direct(32838), source: Relative(1) }, Const { destination: Relative(2), bit_size: Integer(U32), value: 32838 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 1 }, Stop { return_data: HeapVector { pointer: Relative(2), size: Relative(3) } }, Return, Call { location: 23 }, BinaryFieldOp { destination: Relative(3), op: Equals, lhs: Relative(1), rhs: Relative(2) }, Const { destination: Relative(2), bit_size: Integer(U1), value: 0 }, BinaryIntOp { destination: Relative(4), op: Equals, bit_size: U1, lhs: Relative(3), rhs: Relative(2) }, JumpIf { condition: Relative(4), location: 22 }, Const { destination: Relative(5), bit_size: Integer(U32), value: 0 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Relative(5) } }, Return, Const { destination: Direct(32772), bit_size: Integer(U32), value: 30720 }, BinaryIntOp { destination: Direct(32771), op: LessThan, bit_size: U32, lhs: Direct(0), rhs: Direct(32772) }, JumpIf { condition: Direct(32771), location: 28 }, IndirectConst { destination_pointer: Direct(1), bit_size: Integer(U64), value: 15764276373176857197 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Direct(2) } }, Return] + unconstrained func 1 + [Const { destination: Direct(2), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Direct(0), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 2 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(32836), size_address: Relative(3), offset_address: Relative(4) }, Mov { destination: Relative(1), source: Direct(32836) }, Mov { destination: Relative(2), source: Direct(32837) }, Call { location: 14 }, Call { location: 15 }, Mov { destination: Direct(32838), source: Relative(1) }, Const { destination: Relative(2), bit_size: Integer(U32), value: 32838 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 1 }, Stop { return_data: HeapVector { pointer: Relative(2), size: Relative(3) } }, Return, Call { location: 23 }, BinaryFieldOp { destination: Relative(3), op: Equals, lhs: Relative(1), rhs: Relative(2) }, Const { destination: Relative(2), bit_size: Integer(U1), value: 0 }, BinaryIntOp { destination: Relative(4), op: Equals, bit_size: U1, lhs: Relative(3), rhs: Relative(2) }, JumpIf { condition: Relative(4), location: 22 }, Const { destination: Relative(5), bit_size: Integer(U32), value: 0 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Relative(5) } }, Return, Const { destination: Direct(32772), bit_size: Integer(U32), value: 30720 }, BinaryIntOp { destination: Direct(32771), op: LessThan, bit_size: U32, lhs: Direct(0), rhs: Direct(32772) }, JumpIf { condition: Direct(32771), location: 28 }, IndirectConst { destination_pointer: Direct(1), bit_size: Integer(U64), value: 15764276373176857197 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Direct(2) } }, Return] + "); +} + +// Test that given multiple primitive operations that are represented by Brillig directives (e.g. invert/quotient), +// we will only generate one bytecode and the appropriate Brillig call opcodes are generated. +#[test] +fn multiple_brillig_stdlib_calls() { + let src = " + acir(inline) fn main f0 { + b0(v0: u32, v1: u32, v2: u32): + v3 = div v0, v1 + constrain v3 == v2 + v4 = div v1, v2 + constrain v4 == u32 1 + return + }"; + let (program, debug) = ssa_to_acir_program_with_debug_info(src); + // We expect two brillig functions: + // - Quotient (shared between both divisions) + // - Inversion, caused by division-by-zero check (shared between both divisions) + assert_eq!( + program.unconstrained_functions.len(), + 2, + "Should only have generated two Brillig functions" + ); + assert_eq!( + debug[0].brillig_locations.len(), + 0, + "Brillig stdlib functions do not have location information" + ); + + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w10 + private parameters: [w0, w1, w2] + public parameters: [] + return values: [] + BLACKBOX::RANGE [(w0, 32)] [] + BLACKBOX::RANGE [(w1, 32)] [] + BLACKBOX::RANGE [(w2, 32)] [] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w1) 0 ]], outputs: [w3] + EXPR [ (1, w1, w3) -1 ] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w4, w5] + BLACKBOX::RANGE [(w4, 32)] [] + BLACKBOX::RANGE [(w5, 32)] [] + EXPR [ (1, w1) (-1, w5) (-1, w6) -1 ] + BLACKBOX::RANGE [(w6, 32)] [] + EXPR [ (-1, w1, w4) (1, w0) (-1, w5) 0 ] + EXPR [ (-1, w2) (1, w4) 0 ] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w2) 0 ]], outputs: [w7] + EXPR [ (1, w2, w7) -1 ] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w1) 0 ], EXPR [ (1, w2) 0 ]], outputs: [w8, w9] + BLACKBOX::RANGE [(w9, 32)] [] + EXPR [ (1, w2) (-1, w9) (-1, w10) -1 ] + BLACKBOX::RANGE [(w10, 32)] [] + EXPR [ (-1, w2, w8) (1, w1) (-1, w9) 0 ] + EXPR [ (1, w8) -1 ] + + unconstrained func 0 + [Const { destination: Direct(21), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(20), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(21), offset_address: Direct(20) }, Const { destination: Direct(2), bit_size: Field, value: 0 }, BinaryFieldOp { destination: Direct(3), op: Equals, lhs: Direct(0), rhs: Direct(2) }, JumpIf { condition: Direct(3), location: 8 }, Const { destination: Direct(1), bit_size: Field, value: 1 }, BinaryFieldOp { destination: Direct(0), op: Div, lhs: Direct(1), rhs: Direct(0) }, Stop { return_data: HeapVector { pointer: Direct(20), size: Direct(21) } }] + unconstrained func 1 + [Const { destination: Direct(10), bit_size: Integer(U32), value: 2 }, Const { destination: Direct(11), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(10), offset_address: Direct(11) }, BinaryFieldOp { destination: Direct(2), op: IntegerDiv, lhs: Direct(0), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Mul, lhs: Direct(2), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Sub, lhs: Direct(0), rhs: Direct(1) }, Mov { destination: Direct(0), source: Direct(2) }, Stop { return_data: HeapVector { pointer: Direct(11), size: Direct(10) } }] + "); +} + +// Test that given both hardcoded Brillig directives and calls to normal Brillig functions, +// we generate a single bytecode for the directives and a single bytecode for the normal Brillig calls. +#[test] +fn brillig_stdlib_calls_with_regular_brillig_call() { + let src = " + acir(inline) fn main f0 { + b0(v0: u32, v1: u32, v2: u32): + v4 = div v0, v1 + constrain v4 == v2 + v5 = call f1(v0, v1) -> u32 + v6 = call f1(v0, v1) -> u32 + v7 = div v1, v2 + constrain v7 == u32 1 + return + } + brillig(inline) fn foo f1 { + b0(v0: u32, v1: u32): + v2 = eq v0, v1 + constrain v2 == u1 0 + return v0 + } + "; + let (program, debug) = ssa_to_acir_program_with_debug_info(src); + + // We expect 3 brillig functions: + // - Quotient (shared between both divisions) + // - Inversion, caused by division-by-zero check (shared between both divisions) + // - Custom brillig function `foo` + assert_eq!( + program.unconstrained_functions.len(), + 3, + "Should only have generated three Brillig functions" + ); + // We have one normal Brillig functions that was called twice. + // We should have a single locations map for each function's debug metadata. + assert_eq!(debug[0].brillig_locations.len(), 1); + assert!(debug[0].brillig_locations.contains_key(&BrilligFunctionId(0))); + + // Brillig stdlib IDs are expected to always come at the end of the Brillig functions list. + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w12 + private parameters: [w0, w1, w2] + public parameters: [] + return values: [] + BLACKBOX::RANGE [(w0, 32)] [] + BLACKBOX::RANGE [(w1, 32)] [] + BLACKBOX::RANGE [(w2, 32)] [] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w1) 0 ]], outputs: [w3] + EXPR [ (1, w1, w3) -1 ] + BRILLIG CALL func 2: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w4, w5] + BLACKBOX::RANGE [(w4, 32)] [] + BLACKBOX::RANGE [(w5, 32)] [] + EXPR [ (1, w1) (-1, w5) (-1, w6) -1 ] + BLACKBOX::RANGE [(w6, 32)] [] + EXPR [ (-1, w1, w4) (1, w0) (-1, w5) 0 ] + EXPR [ (-1, w2) (1, w4) 0 ] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w7] + BLACKBOX::RANGE [(w7, 32)] [] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w8] + BLACKBOX::RANGE [(w8, 32)] [] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w2) 0 ]], outputs: [w9] + EXPR [ (1, w2, w9) -1 ] + BRILLIG CALL func 2: inputs: [EXPR [ (1, w1) 0 ], EXPR [ (1, w2) 0 ]], outputs: [w10, w11] + BLACKBOX::RANGE [(w11, 32)] [] + EXPR [ (1, w2) (-1, w11) (-1, w12) -1 ] + BLACKBOX::RANGE [(w12, 32)] [] + EXPR [ (-1, w2, w10) (1, w1) (-1, w11) 0 ] + EXPR [ (1, w10) -1 ] + + unconstrained func 0 + [Const { destination: Direct(2), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Direct(0), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 2 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(32836), size_address: Relative(3), offset_address: Relative(4) }, Cast { destination: Direct(32836), source: Direct(32836), bit_size: Integer(U32) }, Cast { destination: Direct(32837), source: Direct(32837), bit_size: Integer(U32) }, Mov { destination: Relative(1), source: Direct(32836) }, Mov { destination: Relative(2), source: Direct(32837) }, Call { location: 16 }, Call { location: 17 }, Mov { destination: Direct(32838), source: Relative(1) }, Const { destination: Relative(2), bit_size: Integer(U32), value: 32838 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 1 }, Stop { return_data: HeapVector { pointer: Relative(2), size: Relative(3) } }, Return, Call { location: 25 }, BinaryIntOp { destination: Relative(3), op: Equals, bit_size: U32, lhs: Relative(1), rhs: Relative(2) }, Const { destination: Relative(2), bit_size: Integer(U1), value: 0 }, BinaryIntOp { destination: Relative(4), op: Equals, bit_size: U1, lhs: Relative(3), rhs: Relative(2) }, JumpIf { condition: Relative(4), location: 24 }, Const { destination: Relative(5), bit_size: Integer(U32), value: 0 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Relative(5) } }, Return, Const { destination: Direct(32772), bit_size: Integer(U32), value: 30720 }, BinaryIntOp { destination: Direct(32771), op: LessThan, bit_size: U32, lhs: Direct(0), rhs: Direct(32772) }, JumpIf { condition: Direct(32771), location: 30 }, IndirectConst { destination_pointer: Direct(1), bit_size: Integer(U64), value: 15764276373176857197 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Direct(2) } }, Return] + unconstrained func 1 + [Const { destination: Direct(21), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(20), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(21), offset_address: Direct(20) }, Const { destination: Direct(2), bit_size: Field, value: 0 }, BinaryFieldOp { destination: Direct(3), op: Equals, lhs: Direct(0), rhs: Direct(2) }, JumpIf { condition: Direct(3), location: 8 }, Const { destination: Direct(1), bit_size: Field, value: 1 }, BinaryFieldOp { destination: Direct(0), op: Div, lhs: Direct(1), rhs: Direct(0) }, Stop { return_data: HeapVector { pointer: Direct(20), size: Direct(21) } }] + unconstrained func 2 + [Const { destination: Direct(10), bit_size: Integer(U32), value: 2 }, Const { destination: Direct(11), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(10), offset_address: Direct(11) }, BinaryFieldOp { destination: Direct(2), op: IntegerDiv, lhs: Direct(0), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Mul, lhs: Direct(2), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Sub, lhs: Direct(0), rhs: Direct(1) }, Mov { destination: Direct(0), source: Direct(2) }, Stop { return_data: HeapVector { pointer: Direct(11), size: Direct(10) } }] + "); +} + +// Test that given both normal Brillig calls, Brillig stdlib calls, and non-inlined ACIR calls, that we accurately generate ACIR. +#[test] +fn brillig_stdlib_calls_with_multiple_acir_calls() { + let src = " + acir(inline) fn main f0 { + b0(v0: u32, v1: u32, v2: u32): + v5 = div v0, v1 + constrain v5 == v2 + v6 = call f1(v0, v1) -> u32 + v7 = call f1(v0, v1) -> u32 + v8 = call f2(v0, v1) -> u32 + v9 = div v1, v2 + constrain v9 == u32 1 + return + } + brillig(inline) fn foo f1 { + b0(v0: u32, v1: u32): + v2 = eq v0, v1 + constrain v2 == u1 0 + return v0 + } + acir(fold) fn foo f2 { + b0(v0: u32, v1: u32): + v2 = eq v0, v1 + constrain v2 == u1 0 + return v0 + } + "; + let (program, debug) = ssa_to_acir_program_with_debug_info(src); + + // We expect 3 brillig functions: + // - Quotient (shared between both divisions) + // - Inversion, caused by division-by-zero check (shared between both divisions) + // - Custom brillig function `foo` + assert_eq!( + program.unconstrained_functions.len(), + 3, + "Should only have generated three Brillig functions" + ); + + let main_debug = &debug[0]; + assert_eq!(main_debug.brillig_locations.len(), 1); + assert!(main_debug.brillig_locations.contains_key(&BrilligFunctionId(0))); + + let foo_debug = &debug[1]; + assert_eq!(foo_debug.brillig_locations.len(), 0); + + // TODO(https://github.com/noir-lang/noir/issues/9877): Update this snapshot once the linked issue is fixed. + // `CALL func 2` in `func 0` is incorrect. + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w13 + private parameters: [w0, w1, w2] + public parameters: [] + return values: [] + BLACKBOX::RANGE [(w0, 32)] [] + BLACKBOX::RANGE [(w1, 32)] [] + BLACKBOX::RANGE [(w2, 32)] [] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w1) 0 ]], outputs: [w3] + EXPR [ (1, w1, w3) -1 ] + BRILLIG CALL func 2: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w4, w5] + BLACKBOX::RANGE [(w4, 32)] [] + BLACKBOX::RANGE [(w5, 32)] [] + EXPR [ (1, w1) (-1, w5) (-1, w6) -1 ] + BLACKBOX::RANGE [(w6, 32)] [] + EXPR [ (-1, w1, w4) (1, w0) (-1, w5) 0 ] + EXPR [ (-1, w2) (1, w4) 0 ] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w7] + BLACKBOX::RANGE [(w7, 32)] [] + BRILLIG CALL func 0: inputs: [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]], outputs: [w8] + BLACKBOX::RANGE [(w8, 32)] [] + CALL func 2: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w9] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w2) 0 ]], outputs: [w10] + EXPR [ (1, w2, w10) -1 ] + BRILLIG CALL func 2: inputs: [EXPR [ (1, w1) 0 ], EXPR [ (1, w2) 0 ]], outputs: [w11, w12] + BLACKBOX::RANGE [(w12, 32)] [] + EXPR [ (1, w2) (-1, w12) (-1, w13) -1 ] + BLACKBOX::RANGE [(w13, 32)] [] + EXPR [ (-1, w2, w11) (1, w1) (-1, w12) 0 ] + EXPR [ (1, w11) -1 ] + + func 1 + current witness: w5 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + BLACKBOX::RANGE [(w0, 32)] [] + BLACKBOX::RANGE [(w1, 32)] [] + EXPR [ (1, w0) (-1, w1) (-1, w3) 0 ] + BRILLIG CALL func 1: inputs: [EXPR [ (1, w3) 0 ]], outputs: [w4] + EXPR [ (1, w3, w4) (1, w5) -1 ] + EXPR [ (1, w3, w5) 0 ] + EXPR [ (1, w5) 0 ] + EXPR [ (-1, w0) (1, w2) 0 ] + + unconstrained func 0 + [Const { destination: Direct(2), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Direct(0), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 2 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(32836), size_address: Relative(3), offset_address: Relative(4) }, Cast { destination: Direct(32836), source: Direct(32836), bit_size: Integer(U32) }, Cast { destination: Direct(32837), source: Direct(32837), bit_size: Integer(U32) }, Mov { destination: Relative(1), source: Direct(32836) }, Mov { destination: Relative(2), source: Direct(32837) }, Call { location: 16 }, Call { location: 17 }, Mov { destination: Direct(32838), source: Relative(1) }, Const { destination: Relative(2), bit_size: Integer(U32), value: 32838 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 1 }, Stop { return_data: HeapVector { pointer: Relative(2), size: Relative(3) } }, Return, Call { location: 25 }, BinaryIntOp { destination: Relative(3), op: Equals, bit_size: U32, lhs: Relative(1), rhs: Relative(2) }, Const { destination: Relative(2), bit_size: Integer(U1), value: 0 }, BinaryIntOp { destination: Relative(4), op: Equals, bit_size: U1, lhs: Relative(3), rhs: Relative(2) }, JumpIf { condition: Relative(4), location: 24 }, Const { destination: Relative(5), bit_size: Integer(U32), value: 0 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Relative(5) } }, Return, Const { destination: Direct(32772), bit_size: Integer(U32), value: 30720 }, BinaryIntOp { destination: Direct(32771), op: LessThan, bit_size: U32, lhs: Direct(0), rhs: Direct(32772) }, JumpIf { condition: Direct(32771), location: 30 }, IndirectConst { destination_pointer: Direct(1), bit_size: Integer(U64), value: 15764276373176857197 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Direct(2) } }, Return] + unconstrained func 1 + [Const { destination: Direct(21), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(20), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(21), offset_address: Direct(20) }, Const { destination: Direct(2), bit_size: Field, value: 0 }, BinaryFieldOp { destination: Direct(3), op: Equals, lhs: Direct(0), rhs: Direct(2) }, JumpIf { condition: Direct(3), location: 8 }, Const { destination: Direct(1), bit_size: Field, value: 1 }, BinaryFieldOp { destination: Direct(0), op: Div, lhs: Direct(1), rhs: Direct(0) }, Stop { return_data: HeapVector { pointer: Direct(20), size: Direct(21) } }] + unconstrained func 2 + [Const { destination: Direct(10), bit_size: Integer(U32), value: 2 }, Const { destination: Direct(11), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(0), size_address: Direct(10), offset_address: Direct(11) }, BinaryFieldOp { destination: Direct(2), op: IntegerDiv, lhs: Direct(0), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Mul, lhs: Direct(2), rhs: Direct(1) }, BinaryFieldOp { destination: Direct(1), op: Sub, lhs: Direct(0), rhs: Direct(1) }, Mov { destination: Direct(0), source: Direct(2) }, Stop { return_data: HeapVector { pointer: Direct(11), size: Direct(10) } }] + "); +} diff --git a/compiler/noirc_evaluator/src/acir/tests/call.rs b/compiler/noirc_evaluator/src/acir/tests/call.rs new file mode 100644 index 00000000000..e8f54842835 --- /dev/null +++ b/compiler/noirc_evaluator/src/acir/tests/call.rs @@ -0,0 +1,177 @@ +use acvm::assert_circuit_snapshot; +use noirc_frontend::monomorphization::ast::InlineType; + +use crate::acir::tests::ssa_to_acir_program; + +/// Check that each `InlineType` which prevents inlining functions generates code in the same manner +#[test] +fn basic_calls_fold() { + basic_call_with_outputs_assert(InlineType::Fold); + call_output_as_next_call_input(InlineType::Fold); + basic_nested_call(InlineType::Fold); +} + +#[test] +#[should_panic = "internal error: entered unreachable code: Expected an associated final index for call to acir function f1 with args [Id(0), Id(1)]"] +fn basic_calls_no_predicates() { + basic_call_with_outputs_assert(InlineType::NoPredicates); +} + +#[test] +#[should_panic = "internal error: entered unreachable code: Expected an associated final index for call to acir function f1 with args [Id(0), Id(1)]"] +fn call_output_as_next_call_input_no_predicates() { + call_output_as_next_call_input(InlineType::NoPredicates); +} + +#[test] +#[should_panic = "internal error: entered unreachable code: Expected an associated final index for call to acir function f1 with args [Id(0), Id(1)]"] +fn nested_call_no_predicates() { + basic_nested_call(InlineType::NoPredicates); +} + +#[test] +#[should_panic = "ICE: Got an ACIR function named foo that should have already been inlined"] +fn call_without_inline_attribute() { + basic_call_with_outputs_assert(InlineType::Inline); +} + +fn basic_call_with_outputs_assert(inline_type: InlineType) { + let src = &format!( + " + acir(inline) fn main f0 {{ + b0(v0: Field, v1: Field): + v3 = call f1(v0, v1) -> Field + v4 = call f1(v0, v1) -> Field + constrain v3 == v4 + return + }} + acir({inline_type}) fn foo f1 {{ + b0(v0: Field, v1: Field): + constrain v0 == v1 + return v0 + }} + " + ); + + let program = ssa_to_acir_program(src); + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w3 + private parameters: [w0, w1] + public parameters: [] + return values: [] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w2] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w3] + EXPR [ (1, w2) (-1, w3) 0 ] + + func 1 + current witness: w2 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + EXPR [ (1, w0) (-1, w1) 0 ] + EXPR [ (-1, w0) (1, w2) 0 ] + "); +} + +fn call_output_as_next_call_input(inline_type: InlineType) { + let src = &format!( + " + acir(inline) fn main f0 {{ + b0(v0: Field, v1: Field): + v3 = call f1(v0, v1) -> Field + v4 = call f1(v3, v1) -> Field + constrain v3 == v4 + return + }} + acir({inline_type}) fn foo f1 {{ + b0(v0: Field, v1: Field): + constrain v0 == v1 + return v0 + }} + " + ); + + let program = ssa_to_acir_program(src); + // The expected result should look very similar to the `basic_call_with_outputs_assert test except that + // the input witnesses of the `Call` opcodes will be different. The differences can discerned from the output below. + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w3 + private parameters: [w0, w1] + public parameters: [] + return values: [] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w2] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w2, w1], outputs: [w3] + EXPR [ (1, w2) (-1, w3) 0 ] + + func 1 + current witness: w2 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + EXPR [ (1, w0) (-1, w1) 0 ] + EXPR [ (-1, w0) (1, w2) 0 ] + "); +} + +fn basic_nested_call(inline_type: InlineType) { + let src = &format!( + " + acir(inline) fn main f0 {{ + b0(v0: Field, v1: Field): + v3 = call f1(v0, v1) -> Field + v4 = call f1(v0, v1) -> Field + constrain v3 == v4 + return + }} + acir({inline_type}) fn func_with_nested_foo_call f1 {{ + b0(v0: Field, v1: Field): + v3 = add v0, Field 2 + v5 = call f2(v3, v1) -> Field + return v5 + }} + acir({inline_type}) fn foo f2 {{ + b0(v0: Field, v1: Field): + constrain v0 == v1 + return v0 + }} + " + ); + + let program = ssa_to_acir_program(src); + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w3 + private parameters: [w0, w1] + public parameters: [] + return values: [] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w2] + CALL func 1: PREDICATE: EXPR [ 1 ] + inputs: [w0, w1], outputs: [w3] + EXPR [ (1, w2) (-1, w3) 0 ] + + func 1 + current witness: w4 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + EXPR [ (1, w0) (-1, w3) 2 ] + CALL func 2: PREDICATE: EXPR [ 1 ] + inputs: [w3, w1], outputs: [w4] + EXPR [ (1, w2) (-1, w4) 0 ] + + func 2 + current witness: w2 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + EXPR [ (1, w0) (-1, w1) 0 ] + EXPR [ (-1, w0) (1, w2) 0 ] + "); +} diff --git a/compiler/noirc_evaluator/src/acir/tests/mod.rs b/compiler/noirc_evaluator/src/acir/tests/mod.rs index 61eb87259e6..0b2451c7c0f 100644 --- a/compiler/noirc_evaluator/src/acir/tests/mod.rs +++ b/compiler/noirc_evaluator/src/acir/tests/mod.rs @@ -2,771 +2,101 @@ use acvm::{ AcirField, FieldElement, acir::{ brillig::{BitSize, HeapVector, IntegerBitSize, MemoryAddress, Opcode as BrilligOpcode}, - circuit::{ - ExpressionWidth, Opcode, OpcodeLocation, - brillig::BrilligFunctionId, - opcodes::{AcirFunctionId, BlackBoxFuncCall}, - }, + circuit::{ExpressionWidth, Program}, native_types::{Witness, WitnessMap}, }, + assert_circuit_snapshot, blackbox_solver::StubbedBlackBoxSolver, pwg::{ACVM, ACVMStatus}, }; -use noirc_errors::Location; -use noirc_frontend::monomorphization::ast::InlineType; +use noirc_errors::debug_info::DebugInfo; +use noirc_frontend::shared::Visibility; use std::collections::BTreeMap; use crate::{ - acir::{BrilligStdlibFunc, acir_context::BrilligStdLib, ssa::codegen_acir}, + acir::{acir_context::BrilligStdLib, ssa::codegen_acir}, brillig::{Brillig, BrilligOptions, brillig_ir::artifact::GeneratedBrillig}, ssa::{ - function_builder::FunctionBuilder, - interpreter::value::Value, - ir::{ - function::FunctionId, - instruction::BinaryOp, - map::Id, - types::{NumericType, Type}, - }, + ArtifactsAndWarnings, combine_artifacts, interpreter::value::Value, ir::types::NumericType, ssa_gen::Ssa, }, }; use proptest::prelude::*; +mod brillig_call; +mod call; mod intrinsics; -fn build_basic_foo_with_return( - builder: &mut FunctionBuilder, - foo_id: FunctionId, - brillig: bool, - inline_type: InlineType, -) { - // fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - if brillig { - builder.new_brillig_function("foo".into(), foo_id, inline_type); - } else { - builder.new_function("foo".into(), foo_id, inline_type); - } - // Set a call stack for testing whether `brillig_locations` in the `GeneratedAcir` was accurately set. - let stack = vec![Location::dummy(), Location::dummy()]; - let call_stack = builder.current_function.dfg.call_stack_data.get_or_insert_locations(&stack); - builder.set_call_stack(call_stack); - - let foo_v0 = builder.add_parameter(Type::field()); - let foo_v1 = builder.add_parameter(Type::field()); - - let foo_equality_check = builder.insert_binary(foo_v0, BinaryOp::Eq, foo_v1); - let zero = builder.numeric_constant(0u128, NumericType::unsigned(1)); - builder.insert_constrain(foo_equality_check, zero, None); - builder.terminate_with_return(vec![foo_v0]); -} - -/// Check that each `InlineType` which prevents inlining functions generates code in the same manner -#[test] -fn basic_calls_fold() { - basic_call_with_outputs_assert(InlineType::Fold); - call_output_as_next_call_input(InlineType::Fold); - basic_nested_call(InlineType::Fold); -} - -#[test] -#[should_panic = "internal error: entered unreachable code: Expected an associated final index for call to acir function f1 with args [Id(0), Id(1)]"] -fn basic_calls_no_predicates() { - basic_call_with_outputs_assert(InlineType::NoPredicates); - call_output_as_next_call_input(InlineType::NoPredicates); - basic_nested_call(InlineType::NoPredicates); -} - -#[test] -#[should_panic = "ICE: Got an ACIR function named foo that should have already been inlined"] -fn call_without_inline_attribute() { - basic_call_with_outputs_assert(InlineType::Inline); -} - -fn basic_call_with_outputs_assert(inline_type: InlineType) { - // acir(inline) fn main f0 { - // b0(v0: Field, v1: Field): - // v2 = call f1(v0, v1) - // v3 = call f1(v0, v1) - // constrain v2 == v3 - // return - // } - // acir(fold) fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::field()); - let main_v1 = builder.add_parameter(Type::field()); - - let foo_id = Id::test_new(1); - let foo = builder.import_function(foo_id); - let main_call1_results = - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - let main_call2_results = - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_constrain(main_call1_results[0], main_call2_results[0], None); - builder.terminate_with_return(vec![]); - - build_basic_foo_with_return(&mut builder, foo_id, false, inline_type); - - let ssa = builder.finish().generate_entry_point_index(); - - let (acir_functions, _, _, _) = ssa - .into_acir(&Brillig::default(), &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - // Expected result: - // main f0 - // GeneratedAcir { - // ... - // opcodes: [ - // CALL func 1: inputs: [Witness(0), Witness(1)], outputs: [Witness(2)], - // CALL func 1: inputs: [Witness(0), Witness(1)], outputs: [Witness(3)], - // EXPR [ (1, _2) (-1, _3) 0 ], - // ], - // return_witnesses: [], - // input_witnesses: [ - // Witness( - // 0, - // ), - // Witness( - // 1, - // ), - // ], - // ... - // } - // foo f1 - // GeneratedAcir { - // ... - // opcodes: [ - // Same as opcodes as the expected result of `basic_call_codegen` - // ], - // return_witnesses: [ - // Witness( - // 0, - // ), - // ], - // input_witnesses: [ - // Witness( - // 0, - // ), - // Witness( - // 1, - // ), - // ], - // ... - // }, - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - assert_eq!(main_opcodes.len(), 3, "Should have two calls to `foo`"); - - check_call_opcode( - &main_opcodes[0], - AcirFunctionId(1), - vec![Witness(0), Witness(1)], - vec![Witness(2)], - ); - check_call_opcode( - &main_opcodes[1], - AcirFunctionId(1), - vec![Witness(0), Witness(1)], - vec![Witness(3)], - ); - - if let Opcode::AssertZero(expr) = &main_opcodes[2] { - assert_eq!(expr.linear_combinations[0].0, FieldElement::from(1u128)); - assert_eq!(expr.linear_combinations[0].1, Witness(2)); - - assert_eq!(expr.linear_combinations[1].0, FieldElement::from(-1i128)); - assert_eq!(expr.linear_combinations[1].1, Witness(3)); - assert_eq!(expr.q_c, FieldElement::from(0u128)); - } +/// Test utility for converting [ACIR gen artifacts][crate::acir::ssa::Artifacts] +/// into the final [ACIR Program][Program] in order to use its parser and human-readable text format. +fn ssa_to_acir_program(src: &str) -> Program { + ssa_to_acir_program_with_debug_info(src).0 } -fn call_output_as_next_call_input(inline_type: InlineType) { - // acir(inline) fn main f0 { - // b0(v0: Field, v1: Field): - // v3 = call f1(v0, v1) - // v4 = call f1(v3, v1) - // constrain v3 == v4 - // return - // } - // acir(fold) fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::field()); - let main_v1 = builder.add_parameter(Type::field()); - - let foo_id = Id::test_new(1); - let foo = builder.import_function(foo_id); - let main_call1_results = - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - let main_call2_results = builder - .insert_call(foo, vec![main_call1_results[0], main_v1], vec![Type::field()]) - .to_vec(); - builder.insert_constrain(main_call1_results[0], main_call2_results[0], None); - builder.terminate_with_return(vec![]); - - build_basic_foo_with_return(&mut builder, foo_id, false, inline_type); - - let ssa = builder.finish(); - - let (acir_functions, _, _, _) = ssa - .generate_entry_point_index() - .into_acir(&Brillig::default(), &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - // The expected result should look very similar to the above test expect that the input witnesses of the `Call` - // opcodes will be different. The changes can discerned from the checks below. - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - assert_eq!(main_opcodes.len(), 3, "Should have two calls to `foo` and an assert"); - - check_call_opcode( - &main_opcodes[0], - AcirFunctionId(1), - vec![Witness(0), Witness(1)], - vec![Witness(2)], - ); - // The output of the first call should be the input of the second call - check_call_opcode( - &main_opcodes[1], - AcirFunctionId(1), - vec![Witness(2), Witness(1)], - vec![Witness(3)], - ); -} - -fn basic_nested_call(inline_type: InlineType) { - // SSA for the following Noir program: - // fn main(x: Field, y: pub Field) { - // let z = func_with_nested_foo_call(x, y); - // let z2 = func_with_nested_foo_call(x, y); - // assert(z == z2); - // } - // #[fold] - // fn func_with_nested_foo_call(x: Field, y: Field) -> Field { - // nested_call(x + 2, y) - // } - // #[fold] - // fn foo(x: Field, y: Field) -> Field { - // assert(x != y); - // x - // } - // - // SSA: - // acir(inline) fn main f0 { - // b0(v0: Field, v1: Field): - // v3 = call f1(v0, v1) - // v4 = call f1(v0, v1) - // constrain v3 == v4 - // return - // } - // acir(fold) fn func_with_nested_foo_call f1 { - // b0(v0: Field, v1: Field): - // v3 = add v0, Field 2 - // v5 = call f2(v3, v1) - // return v5 - // } - // acir(fold) fn foo f2 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == Field 0 - // return v0 - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::field()); - let main_v1 = builder.add_parameter(Type::field()); - - let func_with_nested_foo_call_id = Id::test_new(1); - let func_with_nested_foo_call = builder.import_function(func_with_nested_foo_call_id); - let main_call1_results = builder - .insert_call(func_with_nested_foo_call, vec![main_v0, main_v1], vec![Type::field()]) - .to_vec(); - let main_call2_results = builder - .insert_call(func_with_nested_foo_call, vec![main_v0, main_v1], vec![Type::field()]) - .to_vec(); - builder.insert_constrain(main_call1_results[0], main_call2_results[0], None); - builder.terminate_with_return(vec![]); - - builder.new_function( - "func_with_nested_foo_call".into(), - func_with_nested_foo_call_id, - inline_type, - ); - let func_with_nested_call_v0 = builder.add_parameter(Type::field()); - let func_with_nested_call_v1 = builder.add_parameter(Type::field()); - - let two = builder.field_constant(2u128); - let v0_plus_two = - builder.insert_binary(func_with_nested_call_v0, BinaryOp::Add { unchecked: false }, two); - - let foo_id = Id::test_new(2); - let foo_call = builder.import_function(foo_id); - let foo_call = builder - .insert_call(foo_call, vec![v0_plus_two, func_with_nested_call_v1], vec![Type::field()]) - .to_vec(); - builder.terminate_with_return(vec![foo_call[0]]); - - build_basic_foo_with_return(&mut builder, foo_id, false, inline_type); - - let ssa = builder.finish().generate_entry_point_index(); - - let (acir_functions, _, _, _) = ssa - .into_acir(&Brillig::default(), &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - - assert_eq!(acir_functions.len(), 3, "Should have three ACIR functions"); - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - assert_eq!(main_opcodes.len(), 3, "Should have two calls to `foo` and an assert"); - - // Both of these should call func_with_nested_foo_call f1 - check_call_opcode( - &main_opcodes[0], - AcirFunctionId(1), - vec![Witness(0), Witness(1)], - vec![Witness(2)], - ); - // The output of the first call should be the input of the second call - check_call_opcode( - &main_opcodes[1], - AcirFunctionId(1), - vec![Witness(0), Witness(1)], - vec![Witness(3)], - ); - - let func_with_nested_call_acir = &acir_functions[1]; - let func_with_nested_call_opcodes = func_with_nested_call_acir.opcodes(); - - assert_eq!( - func_with_nested_call_opcodes.len(), - 3, - "Should have an expression and a call to a nested `foo`" - ); - // Should call foo f2 - check_call_opcode( - &func_with_nested_call_opcodes[1], - AcirFunctionId(2), - vec![Witness(3), Witness(1)], - vec![Witness(4)], - ); -} - -fn check_call_opcode( - opcode: &Opcode, - expected_id: AcirFunctionId, - expected_inputs: Vec, - expected_outputs: Vec, -) { - match opcode { - Opcode::Call { id, inputs, outputs, .. } => { - assert_eq!(*id, expected_id, "Main was expected to call {expected_id} but got {}", *id); - for (expected_input, input) in expected_inputs.iter().zip(inputs) { - assert_eq!( - expected_input, input, - "Expected input witness {expected_input:?} but got {input:?}" - ); - } - for (expected_output, output) in expected_outputs.iter().zip(outputs) { - assert_eq!( - expected_output, output, - "Expected output witness {expected_output:?} but got {output:?}" - ); - } - } - _ => panic!("Expected only Call opcode"), - } -} - -// Test that given multiple calls to the same brillig function we generate only one bytecode -// and the appropriate Brillig call opcodes are generated -#[test] -fn multiple_brillig_calls_one_bytecode() { - // acir(inline) fn main f0 { - // b0(v0: Field, v1: Field): - // v4 = call f1(v0, v1) - // v5 = call f1(v0, v1) - // v6 = call f1(v0, v1) - // v7 = call f2(v0, v1) - // v8 = call f1(v0, v1) - // v9 = call f2(v0, v1) - // return - // } - // brillig fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - // brillig fn foo f2 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::field()); - let main_v1 = builder.add_parameter(Type::field()); - - let foo_id = Id::test_new(1); - let foo = builder.import_function(foo_id); - let bar_id = Id::test_new(2); - let bar = builder.import_function(bar_id); - - // Insert multiple calls to the same Brillig function - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - // Interleave a call to a separate Brillig function to make sure that we can call multiple separate Brillig functions - builder.insert_call(bar, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(bar, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.terminate_with_return(vec![]); - - build_basic_foo_with_return(&mut builder, foo_id, true, InlineType::default()); - build_basic_foo_with_return(&mut builder, bar_id, true, InlineType::default()); - - let ssa = builder.finish(); - let brillig = ssa.to_brillig(&BrilligOptions::default()); - - let (acir_functions, brillig_functions, _, _) = ssa - .generate_entry_point_index() - .into_acir(&brillig, &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - - assert_eq!(acir_functions.len(), 1, "Should only have a `main` ACIR function"); - assert_eq!(brillig_functions.len(), 2, "Should only have generated two Brillig functions"); - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - assert_eq!(main_opcodes.len(), 6, "Should have four calls to f1 and two calls to f2"); - - // We should only have `BrilligCall` opcodes in `main` - for (i, opcode) in main_opcodes.iter().enumerate() { - match opcode { - Opcode::BrilligCall { id, .. } => { - let expected_id = if i == 3 || i == 5 { 1 } else { 0 }; - let expected_id = BrilligFunctionId(expected_id); - assert_eq!(*id, expected_id, "Expected an id of {expected_id} but got {id}"); - } - _ => panic!("Expected only Brillig call opcode"), - } - } - - // We have two normal Brillig functions that was called multiple times. - // We should have a single locations map for each function's debug metadata. - assert_eq!(main_acir.brillig_locations.len(), 2); - assert!(main_acir.brillig_locations.contains_key(&BrilligFunctionId(0))); - assert!(main_acir.brillig_locations.contains_key(&BrilligFunctionId(1))); -} - -// Test that given multiple primitive operations that are represented by Brillig directives (e.g. invert/quotient), -// we will only generate one bytecode and the appropriate Brillig call opcodes are generated. -#[test] -fn multiple_brillig_stdlib_calls() { - let src = " - acir(inline) fn main f0 { - b0(v0: u32, v1: u32, v2: u32): - v3 = div v0, v1 - constrain v3 == v2 - v4 = div v1, v2 - constrain v4 == u32 1 - return - }"; +fn ssa_to_acir_program_with_debug_info(src: &str) -> (Program, Vec) { let ssa = Ssa::from_str(src).unwrap(); + let arg_size_and_visibilities = ssa + .functions + .iter() + .filter(|(id, function)| { + function.runtime().is_acir() + && (**id == ssa.main_id || function.runtime().is_entry_point()) + }) + .map(|(_, function)| { + // Make all arguments private for the sake of simplicity. + let param_size: u32 = function + .parameters() + .iter() + .map(|param| function.dfg.type_of_value(*param).flattened_size()) + .sum(); + vec![(param_size, Visibility::Private)] + }) + .collect::>(); - // The Brillig bytecode we insert for the stdlib is hardcoded so we do not need to provide any - // Brillig artifacts to the ACIR gen pass. - let (acir_functions, brillig_functions, _, _) = ssa - .generate_entry_point_index() - .into_acir(&Brillig::default(), &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - - assert_eq!(acir_functions.len(), 1, "Should only have a `main` ACIR function"); - // We expect two brillig functions: - // - Quotient (shared between both divisions) - // - Inversion, caused by division-by-zero check (shared between both divisions) - assert_eq!(brillig_functions.len(), 2, "Should only have generated two Brillig functions"); - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - check_brillig_calls(&acir_functions[0].brillig_stdlib_func_locations, main_opcodes, 0, 4, 0); - - assert_eq!(main_acir.brillig_locations.len(), 0); -} - -// Test that given both hardcoded Brillig directives and calls to normal Brillig functions, -// we generate a single bytecode for the directives and a single bytecode for the normal Brillig calls. -#[test] -fn brillig_stdlib_calls_with_regular_brillig_call() { - // acir(inline) fn main f0 { - // b0(v0: u32, v1: u32, v2: u32): - // v4 = div v0, v1 - // constrain v4 == v2 - // v5 = call f1(v0, v1) - // v6 = call f1(v0, v1) - // v7 = div v1, v2 - // constrain v7 == u32 1 - // return - // } - // brillig fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::unsigned(32)); - let main_v1 = builder.add_parameter(Type::unsigned(32)); - let main_v2 = builder.add_parameter(Type::unsigned(32)); - - let foo_id = Id::test_new(1); - let foo = builder.import_function(foo_id); - - // Call a primitive operation that uses Brillig - let v0_div_v1 = builder.insert_binary(main_v0, BinaryOp::Div, main_v1); - builder.insert_constrain(v0_div_v1, main_v2, None); - - // Insert multiple calls to the same Brillig function - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - - // Call the same primitive operation again - let v1_div_v2 = builder.insert_binary(main_v1, BinaryOp::Div, main_v2); - let one = builder.numeric_constant(1u128, NumericType::unsigned(32)); - builder.insert_constrain(v1_div_v2, one, None); - - builder.terminate_with_return(vec![]); - - build_basic_foo_with_return(&mut builder, foo_id, true, InlineType::default()); - - let ssa = builder.finish(); - // We need to generate Brillig artifacts for the regular Brillig function and pass them to the ACIR generation pass. - let brillig = ssa.to_brillig(&BrilligOptions::default()); - - let (acir_functions, brillig_functions, _, _) = ssa - .generate_entry_point_index() - .into_acir(&brillig, &BrilligOptions::default(), ExpressionWidth::default()) - .expect("Should compile manually written SSA into ACIR"); - - assert_eq!(acir_functions.len(), 1, "Should only have a `main` ACIR function"); - // We expect 3 brillig functions: - // - Quotient (shared between both divisions) - // - Inversion, caused by division-by-zero check (shared between both divisions) - // - Custom brillig function `foo` - assert_eq!(brillig_functions.len(), 3, "Should only have generated three Brillig functions"); - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - check_brillig_calls(&main_acir.brillig_stdlib_func_locations, main_opcodes, 1, 4, 2); - - // We have one normal Brillig functions that was called twice. - // We should have a single locations map for each function's debug metadata. - assert_eq!(main_acir.brillig_locations.len(), 1); - assert!(main_acir.brillig_locations.contains_key(&BrilligFunctionId(0))); -} - -// Test that given both normal Brillig calls, Brillig stdlib calls, and non-inlined ACIR calls, that we accurately generate ACIR. -#[test] -fn brillig_stdlib_calls_with_multiple_acir_calls() { - // acir(inline) fn main f0 { - // b0(v0: u32, v1: u32, v2: u32): - // v4 = div v0, v1 - // constrain v4 == v2 - // v5 = call f1(v0, v1) - // v6 = call f2(v0, v1) - // v7 = div v1, v2 - // constrain v7 == u32 1 - // return - // } - // brillig fn foo f1 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - // acir(fold) fn foo f2 { - // b0(v0: Field, v1: Field): - // v2 = eq v0, v1 - // constrain v2 == u1 0 - // return v0 - // } - // } - let foo_id = Id::test_new(0); - let mut builder = FunctionBuilder::new("main".into(), foo_id); - let main_v0 = builder.add_parameter(Type::unsigned(32)); - let main_v1 = builder.add_parameter(Type::unsigned(32)); - let main_v2 = builder.add_parameter(Type::unsigned(32)); - - let foo_id = Id::test_new(1); - let foo = builder.import_function(foo_id); - let bar_id = Id::test_new(2); - let bar = builder.import_function(bar_id); - - // Call a primitive operation that uses Brillig - let v0_div_v1 = builder.insert_binary(main_v0, BinaryOp::Div, main_v1); - builder.insert_constrain(v0_div_v1, main_v2, None); - - // Insert multiple calls to the same Brillig function - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(foo, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - builder.insert_call(bar, vec![main_v0, main_v1], vec![Type::field()]).to_vec(); - - // Call the same primitive operation again - let v1_div_v2 = builder.insert_binary(main_v1, BinaryOp::Div, main_v2); - let one = builder.numeric_constant(1u128, NumericType::unsigned(32)); - builder.insert_constrain(v1_div_v2, one, None); - - builder.terminate_with_return(vec![]); - - // Build a Brillig function - build_basic_foo_with_return(&mut builder, foo_id, true, InlineType::default()); - // Build an ACIR function which has the same logic as the Brillig function above - build_basic_foo_with_return(&mut builder, bar_id, false, InlineType::Fold); - - let ssa = builder.finish(); - // We need to generate Brillig artifacts for the regular Brillig function and pass them to the ACIR generation pass. let brillig = ssa.to_brillig(&BrilligOptions::default()); - let (acir_functions, brillig_functions, _, _) = ssa - .generate_entry_point_index() + let (acir_functions, brillig_functions, brillig_names, _) = ssa .into_acir(&brillig, &BrilligOptions::default(), ExpressionWidth::default()) .expect("Should compile manually written SSA into ACIR"); - assert_eq!(acir_functions.len(), 2, "Should only have two ACIR functions"); - // We expect 3 brillig functions: - // - Quotient (shared between both divisions) - // - Inversion, caused by division-by-zero check (shared between both divisions) - // - Custom brillig function `foo` - assert_eq!(brillig_functions.len(), 3, "Should only have generated three Brillig functions"); - - let main_acir = &acir_functions[0]; - let main_opcodes = main_acir.opcodes(); - check_brillig_calls(&acir_functions[0].brillig_stdlib_func_locations, main_opcodes, 1, 4, 2); - - assert_eq!(main_acir.brillig_locations.len(), 1); - assert!(main_acir.brillig_locations.contains_key(&BrilligFunctionId(0))); - - let foo_acir = &acir_functions[1]; - let foo_opcodes = foo_acir.opcodes(); - check_brillig_calls(&acir_functions[1].brillig_stdlib_func_locations, foo_opcodes, 1, 1, 0); - - assert_eq!(foo_acir.brillig_locations.len(), 0); -} - -fn check_brillig_calls( - brillig_stdlib_function_locations: &BTreeMap, - opcodes: &[Opcode], - num_normal_brillig_functions: u32, - expected_num_stdlib_calls: u32, - expected_num_normal_calls: u32, -) { - // First we check calls to the Brillig stdlib - let mut num_brillig_stdlib_calls = 0; - for (i, (opcode_location, brillig_stdlib_func)) in - brillig_stdlib_function_locations.iter().enumerate() - { - // We can take just modulo 2 to determine the expected ID as we only code generated two Brillig stdlib function - let stdlib_func_index = (i % 2) as u32; - if stdlib_func_index == 0 { - assert!(matches!(brillig_stdlib_func, BrilligStdlibFunc::Inverse)); - } else { - assert!(matches!(brillig_stdlib_func, BrilligStdlibFunc::Quotient)); - } - - match opcode_location { - OpcodeLocation::Acir(acir_index) => { - match opcodes[*acir_index] { - Opcode::BrilligCall { id, .. } => { - // Brillig stdlib function calls are only resolved at the end of ACIR generation so their - // IDs are expected to always reference Brillig bytecode at the end of the Brillig functions list. - // We have one normal Brillig call so we add one here to the std lib function's index within the std lib. - let expected_id = stdlib_func_index + num_normal_brillig_functions; - let expected_id = BrilligFunctionId(expected_id); - assert_eq!(id, expected_id, "Expected {expected_id} but got {id}"); - num_brillig_stdlib_calls += 1; - } - _ => panic!("Expected BrilligCall opcode"), - } - } - _ => panic!("Expected OpcodeLocation::Acir"), - } - } - - assert_eq!( - num_brillig_stdlib_calls, expected_num_stdlib_calls, - "Should have {expected_num_stdlib_calls} BrilligCall opcodes to stdlib functions but got {num_brillig_stdlib_calls}" + let artifacts = ArtifactsAndWarnings( + (acir_functions, brillig_functions, brillig_names, BTreeMap::default()), + vec![], ); - - // Check the normal Brillig calls - // This check right now expects to only call one Brillig function. - let mut num_normal_brillig_calls = 0; - for (i, opcode) in opcodes.iter().enumerate() { - if let Opcode::BrilligCall { id, .. } = opcode { - if brillig_stdlib_function_locations.get(&OpcodeLocation::Acir(i)).is_some() { - // We should have already checked Brillig stdlib functions and only want to check normal Brillig calls here - continue; - } - // We only generate one normal Brillig call so we should expect a function ID of `0` - let expected_id = BrilligFunctionId(0); - assert_eq!(*id, expected_id, "Expected an id of {expected_id} but got {id}"); - num_normal_brillig_calls += 1; - } - } - - assert_eq!( - num_normal_brillig_calls, expected_num_normal_calls, - "Should have {expected_num_normal_calls} BrilligCall opcodes to normal Brillig functions but got {num_normal_brillig_calls}" + let program_artifact = combine_artifacts( + artifacts, + &arg_size_and_visibilities, + BTreeMap::default(), + BTreeMap::default(), + BTreeMap::default(), ); + let program = program_artifact.program; + let debug = program_artifact.debug; + (program, debug) } #[test] fn unchecked_mul_should_not_have_range_check() { let src = " - acir(inline) fn main f0 { - b0(v0: u32, v1: u32): - v3 = unchecked_mul v0, v1 - return v3 - } - "; - let ssa = Ssa::from_str(src).unwrap(); - let brillig = ssa.to_brillig(&BrilligOptions::default()); - - let (mut 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 opcodes = acir_functions[0].take_opcodes(); - - for opcode in opcodes { - if let Opcode::BlackBoxFuncCall(BlackBoxFuncCall::RANGE { input }) = opcode { - assert!( - input.to_witness().0 <= 1, - "only input witnesses should have range checks: {opcode:?}" - ); + acir(inline) fn main f0 { + b0(v0: u32, v1: u32): + v3 = unchecked_mul v0, v1 + return v3 } - } + "; + let program = ssa_to_acir_program(src); + + // Check that range checks only exist on the function parameters + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w2 + private parameters: [w0, w1] + public parameters: [] + return values: [w2] + BLACKBOX::RANGE [(w0, 32)] [] + BLACKBOX::RANGE [(w1, 32)] [] + EXPR [ (-1, w0, w1) (1, w2) 0 ] + "); } #[test] @@ -786,18 +116,22 @@ fn does_not_generate_memory_blocks_without_dynamic_accesses() { return } "; - let ssa = Ssa::from_str(src).unwrap(); - 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 program = ssa_to_acir_program(src); + println!("{program}"); // Check that no memory opcodes were emitted. - let main = &acir_functions[0]; - assert!(!main.opcodes().iter().any(|opcode| matches!(opcode, Opcode::MemoryOp { .. }))); + assert_circuit_snapshot!(program, @r" + func 0 + current witness: w1 + private parameters: [w0, w1] + public parameters: [] + return values: [] + BRILLIG CALL func 0: inputs: [EXPR [ 2 ], [EXPR [ (1, w0) 0 ], EXPR [ (1, w1) 0 ]]], outputs: [] + EXPR [ (1, w0) 0 ] + + unconstrained func 0 + [Const { destination: Direct(2), bit_size: Integer(U32), value: 1 }, Const { destination: Direct(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Direct(0), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(3), bit_size: Integer(U32), value: 3 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 0 }, CalldataCopy { destination_address: Direct(32836), size_address: Relative(3), offset_address: Relative(4) }, Cast { destination: Direct(32836), source: Direct(32836), bit_size: Integer(U32) }, Mov { destination: Relative(1), source: Direct(32836) }, Const { destination: Relative(2), bit_size: Integer(U32), value: 32837 }, Const { destination: Relative(4), bit_size: Integer(U32), value: 2 }, Const { destination: Relative(6), bit_size: Integer(U32), value: 3 }, BinaryIntOp { destination: Relative(5), op: Add, bit_size: U32, lhs: Relative(4), rhs: Relative(6) }, Mov { destination: Relative(3), source: Direct(1) }, BinaryIntOp { destination: Direct(1), op: Add, bit_size: U32, lhs: Direct(1), rhs: Relative(5) }, IndirectConst { destination_pointer: Relative(3), bit_size: Integer(U32), value: 1 }, BinaryIntOp { destination: Relative(5), op: Add, bit_size: U32, lhs: Relative(3), rhs: Direct(2) }, Store { destination_pointer: Relative(5), source: Relative(4) }, BinaryIntOp { destination: Relative(5), op: Add, bit_size: U32, lhs: Relative(5), rhs: Direct(2) }, Store { destination_pointer: Relative(5), source: Relative(4) }, Const { destination: Relative(6), bit_size: Integer(U32), value: 3 }, BinaryIntOp { destination: Relative(5), op: Add, bit_size: U32, lhs: Relative(3), rhs: Relative(6) }, Mov { destination: Direct(32771), source: Relative(2) }, Mov { destination: Direct(32772), source: Relative(5) }, Mov { destination: Direct(32773), source: Relative(4) }, Call { location: 31 }, Mov { destination: Relative(2), source: Relative(3) }, Call { location: 42 }, Call { location: 43 }, Const { destination: Relative(1), bit_size: Integer(U32), value: 32839 }, Const { destination: Relative(2), bit_size: Integer(U32), value: 0 }, Stop { return_data: HeapVector { pointer: Relative(1), size: Relative(2) } }, BinaryIntOp { destination: Direct(32775), op: Add, bit_size: U32, lhs: Direct(32771), rhs: Direct(32773) }, Mov { destination: Direct(32776), source: Direct(32771) }, Mov { destination: Direct(32777), source: Direct(32772) }, BinaryIntOp { destination: Direct(32778), op: Equals, bit_size: U32, lhs: Direct(32776), rhs: Direct(32775) }, JumpIf { condition: Direct(32778), location: 41 }, Load { destination: Direct(32774), source_pointer: Direct(32776) }, Store { destination_pointer: Direct(32777), source: Direct(32774) }, BinaryIntOp { destination: Direct(32776), op: Add, bit_size: U32, lhs: Direct(32776), rhs: Direct(2) }, BinaryIntOp { destination: Direct(32777), op: Add, bit_size: U32, lhs: Direct(32777), rhs: Direct(2) }, Jump { location: 34 }, Return, Return, Call { location: 45 }, Return, Const { destination: Direct(32772), bit_size: Integer(U32), value: 30720 }, BinaryIntOp { destination: Direct(32771), op: LessThan, bit_size: U32, lhs: Direct(0), rhs: Direct(32772) }, JumpIf { condition: Direct(32771), location: 50 }, IndirectConst { destination_pointer: Direct(1), bit_size: Integer(U64), value: 15764276373176857197 }, Trap { revert_data: HeapVector { pointer: Direct(1), size: Direct(2) } }, Return] + "); } #[test] @@ -915,7 +249,6 @@ fn do_not_overflow_with_constant_constrain_neq() { return } "#; - let ssa = Ssa::from_str(src).unwrap(); let brillig = ssa.to_brillig(&BrilligOptions::default()); @@ -931,21 +264,47 @@ fn do_not_overflow_with_constant_constrain_neq() { fn derive_pedersen_generators_requires_constant_input() { // derive_pedersen_generators is expected to fail because one of its argument is not a constant. let src = r#" - acir(inline) fn main f0 { - b0(v0: u32, v1: u32): - separator = make_array b"DEFAULT_DOMAIN_SEPARATOR" - v2 = call derive_pedersen_generators(separator, v1) -> [(Field, Field, u1); 1] - return v2 - } - "#; + acir(inline) fn main f0 { + b0(v0: u32, v1: u32): + separator = make_array b"DEFAULT_DOMAIN_SEPARATOR" + v2 = call derive_pedersen_generators(separator, v1) -> [(Field, Field, u1); 1] + return v2 + } + "#; let ssa = Ssa::from_str(src).unwrap(); let brillig = ssa.to_brillig(&BrilligOptions::default()); - let ssa = ssa.fold_constants(); ssa.into_acir(&brillig, &BrilligOptions::default(), ExpressionWidth::default()) .expect_err("Should fail with assert constant"); } +#[test] +// Regression for https://github.com/noir-lang/noir/issues/9847 +fn signed_div_overflow() { + // Test that check -128 / -1 overflow for i8 + let src = r#" + acir(inline) predicate_pure fn main f0 { + b0(v1: i8, v2: i8): + v3 = div v1, v2 + return + } + "#; + + let ssa = Ssa::from_str(src).unwrap(); + let inputs = vec![FieldElement::from(128_u128), FieldElement::from(255_u128)]; + let inputs = inputs + .into_iter() + .enumerate() + .map(|(i, f)| (Witness(i as u32), f)) + .collect::>(); + let initial_witness = WitnessMap::from(inputs); + let output = None; + + // acir execution should fail to divide -128 / -1 + let acir_execution_result = execute_ssa(ssa, initial_witness.clone(), output.as_ref()); + assert!(matches!(acir_execution_result, (ACVMStatus::Failure(_), _))); +} + /// 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. @@ -1082,33 +441,6 @@ fn test_operators( } } -#[test] -// Regression for https://github.com/noir-lang/noir/issues/9847 -fn signed_div_overflow() { - // Test that check -128 / -1 overflow for i8 - let src = r#" - acir(inline) predicate_pure fn main f0 { - b0(v1: i8, v2: i8): - v3 = div v1, v2 - return - } - "#; - - let ssa = Ssa::from_str(src).unwrap(); - let inputs = vec![FieldElement::from(128_u128), FieldElement::from(255_u128)]; - let inputs = inputs - .into_iter() - .enumerate() - .map(|(i, f)| (Witness(i as u32), f)) - .collect::>(); - let initial_witness = WitnessMap::from(inputs); - let output = None; - - // acir execution should fail to divide -128 / -1 - let acir_execution_result = execute_ssa(ssa, initial_witness.clone(), output.as_ref()); - assert!(matches!(acir_execution_result, (ACVMStatus::Failure(_), _))); -} - proptest! { #[test] fn test_binary_on_field(lhs in 0u128.., rhs in 0u128..) { diff --git a/compiler/noirc_evaluator/src/ssa/parser/into_ssa.rs b/compiler/noirc_evaluator/src/ssa/parser/into_ssa.rs index bfe81b6feef..159264ad1e3 100644 --- a/compiler/noirc_evaluator/src/ssa/parser/into_ssa.rs +++ b/compiler/noirc_evaluator/src/ssa/parser/into_ssa.rs @@ -4,7 +4,7 @@ use std::{ }; use acvm::acir::circuit::ErrorSelector; -use noirc_errors::call_stack::CallStackId; +use noirc_errors::{Location, call_stack::CallStackId}; use crate::ssa::{ function_builder::FunctionBuilder, @@ -156,10 +156,18 @@ impl Translator { RuntimeType::Brillig(inline_type) => { self.builder.new_brillig_function(external_name, function_id, inline_type); self.builder.set_globals(self.globals_graph.clone()); + + // In our ACIR generation tests we want to make sure that `brillig_locations` in the `GeneratedAcir` was accurately set. + // Thus, we set a dummy location here so that translated instructions have a location associated with them. + let stack = vec![Location::dummy()]; + let call_stack_data = &mut self.builder.current_function.dfg.call_stack_data; + let call_stack = call_stack_data.get_or_insert_locations(&stack); + self.builder.set_call_stack(call_stack); } } self.builder.set_purities(self.purities.clone()); + self.translate_function_body(function) } @@ -573,7 +581,7 @@ impl Translator { } fn finish(self) -> Ssa { - let mut ssa = self.builder.finish(); + let mut ssa = self.builder.finish().generate_entry_point_index(); // Normalize the IDs so we have a better chance of matching the SSA we parsed // after the step-by-step reconstruction done during translation. This assumes