acvm/pwg/
mod.rs

1// Re-usable methods that backends can use to implement their PWG
2
3//! This module contains methods to implement the partial witness generation (PWG) of an ACIR program.
4//! The goal of ACIR execution is to compute the values of all the ACIR witnesses, or an error if it could not compute them all.
5//! A proving system will then be able to use the ACIR circuit and the values of the ACIR witnesses to generate a proof of this execution.
6//! The ACIR opcodes are not modified by the execution.
7//! Witness generation means getting valid values for the witnesses used by the ACIR opcodes of the program.
8//! They are called *partial* witness because a proving system may create additional witnesses on its own for
9//! generating the proof (and a corresponding low-level circuit). The PWG generates values for all the witnesses
10//! of the ACIR program, or returns an error if it cannot do it.
11//!
12//! Implementation details & examples:
13//! It starts by instantiating an ACVM (ACIR Virtual Machine), which executes the given ACIR opcodes in the `solve()` function.
14//!
15//! Parameters: When instantiating the ACVM, it needs to be provided with:
16//!  - a `backend` implementing the `BlackBoxFunctionSolver` trait. Different implementation can be used depending on the EC used by the underlying proving system.
17//!  - `opcodes`: the ACIR opcodes of the program to solve.
18//!  - `initial_witness`: a mapping of initial witness values representing the inputs of the program. The ACVM will update this map as it solves the opcodes.
19//!  - `unconstrained_functions`: the Brillig bytecode of the unconstrained functions used by the program.
20//!  - `assertion_payloads`: additional information used to provide feedback to the user when an assertion fails.
21//!
22//! Returns: [`ACVMStatus`]
23//!
24//! Each opcode is solved independently. In general we require its inputs to be already known, i.e previously solved,
25//! and the output is simply computed from the inputs, and then the output becomes 'known' for the subsequent opcodes.
26//!
27//! See [`acir::circuit::Opcode`] for more details.
28//!
29//! Example:
30// Compiled ACIR for main (non-transformed):
31// func 0
32// private parameters: [w0, w1, w2, w3, w4]
33// public parameters: []
34// return values: [w9]
35// BLACKBOX::RANGE input: w0, bits: 32
36// BLACKBOX::RANGE input: w1, bits: 32
37// BLACKBOX::RANGE input: w2, bits: 32
38// BLACKBOX::RANGE input: w3, bits: 32
39// BLACKBOX::RANGE input: w4, bits: 32
40// ASSERT w0 - w1 - w6 = 0
41// BRILLIG CALL func: 0, predicate: 1, inputs: [w6], outputs: [w7]
42// ASSERT w6*w7 + w8 - 1 = 0
43// ASSERT w6*w8 = 0
44// ASSERT w1*w8 = 0
45// ASSERT w0 - w2 - w9 = 0
46//!
47//! This ACIR program defines the 'main' function and indicates it is 'non-transformed'.
48//! Indeed, some ACIR pass can transform the ACIR program in order to apply optimizations,
49//! or to make it compatible with a specific proving system.
50//! However, ACIR execution is expected to work on any ACIR program (transformed or not).
51//! Then we see the parameters of the program as public and private inputs.
52//! The `initial_witness` needs to contain values for these parameters before execution, else
53//! the execution will fail.
54//! The first ACIR opcodes are RANGE opcodes which ensure the inputs have the expected range (as specified in the Noir source code).
55//! Solving this black-box simply means to validate that the values (from `initial_witness`) are indeed 32 bits for w0, w1, w2, w3, w4
56//! If `initial_witness` does not have values for w0, w1, w2, w3, w4, or if the values are over 32 bits, the execution will fail.
57//! The next opcode is an `AssertZero` opcode: ASSERT w0 - w1 - w6 = 0, which indicates that `w0 - w1 - w6` should be equal to 0.
58//! Since we know the values of `w0, w1` from `initial_witness`, we can compute `w6 = w0 + w1` so that the `AssertZero` is satisfied.
59//! Solving `AssertZero` means computing the unknown witness and adding the result to `initial_witness`, which now contains the value for `w6`.
60//! The next opcode is a Brillig Call where input is `w6` and output is `w7`. From the function id of the opcode, the solver will retrieve the
61//! corresponding Brillig bytecode and instantiate a Brillig VM with the value of the input. This value was just computed before.
62//! Executing the Brillig VM on this input will give us the output which is the value for `w7`, that we add to `initial_witness`.
63//! The next opcode is again an `AssertZero`: `w6 * w7 + w8 - 1 = 0`, which computes the value of `w8`.
64//! The two next opcodes are `AssertZero` without any unknown witnesses: `w6 * w8 = 0` and `w1 * w8 = 0`
65//! Solving such opcodes means that we compute `w6 * w8 ` and `w1 * w8` using the known values, and check that they evaluate to 0.
66//! If not, we would return an error.
67//! Finally, the last `AssertZero` computes `w9` which is the last witness. All of the witnesses have now been computed; execution is complete.
68
69use std::collections::HashMap;
70
71use acir::{
72    AcirField, BlackBoxFunc,
73    brillig::ForeignCallResult,
74    circuit::{
75        AssertionPayload, ErrorSelector, ExpressionOrMemory, Opcode, OpcodeLocation,
76        brillig::{BrilligBytecode, BrilligFunctionId, BrilligInputs, BrilligOutputs},
77        opcodes::{AcirFunctionId, BlockId, FunctionInput, InvalidInputBitSize},
78    },
79    native_types::{Expression, Witness, WitnessMap},
80};
81use acvm_blackbox_solver::BlackBoxResolutionError;
82use brillig_vm::fuzzing::BranchToFeatureMap;
83use itertools::Itertools;
84
85use self::{arithmetic::ExpressionSolver, memory_op::MemoryOpSolver};
86use crate::BlackBoxFunctionSolver;
87
88use thiserror::Error;
89
90// arithmetic
91pub(crate) mod arithmetic;
92// Brillig bytecode
93pub(crate) mod brillig;
94// black box functions
95pub(crate) mod blackbox;
96pub(crate) mod memory_op;
97
98pub use self::brillig::{BrilligSolver, BrilligSolverStatus};
99pub use brillig::ForeignCallWaitInfo;
100use serde::{Deserialize, Serialize};
101
102#[derive(Debug, Clone, PartialEq)]
103pub enum ACVMStatus<F> {
104    /// All witnesses have been computed and all opcodes have been successfully resolved. Execution is complete.
105    Solved,
106
107    /// The ACVM is processing the circuit, i.e solving the opcodes. This status is used to resume execution after it has been paused.
108    InProgress,
109
110    /// The ACVM has encountered an irrecoverable error while executing the circuit and can not progress.
111    /// Most commonly this will be due to an unsatisfied constraint due to invalid inputs to the circuit.
112    Failure(OpcodeResolutionError<F>),
113
114    /// The ACVM has encountered a request for a Brillig [foreign call][brillig_vm::brillig::Opcode::ForeignCall]
115    /// to retrieve information from outside of the ACVM. The result of the foreign call must be passed back
116    /// to the ACVM using [`ACVM::resolve_pending_foreign_call`].
117    ///
118    /// Once this is done, the ACVM can be restarted to solve the remaining opcodes.
119    RequiresForeignCall(ForeignCallWaitInfo<F>),
120
121    /// The ACVM has encountered a request for an ACIR [call][acir::circuit::Opcode]
122    /// to execute a separate ACVM instance. The result of the ACIR call must be passed back to the ACVM.
123    ///
124    /// Once this is done, the ACVM can be restarted to solve the remaining opcodes.
125    RequiresAcirCall(AcirCallWaitInfo<F>),
126}
127
128impl<F> std::fmt::Display for ACVMStatus<F> {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            ACVMStatus::Solved => write!(f, "Solved"),
132            ACVMStatus::InProgress => write!(f, "In progress"),
133            ACVMStatus::Failure(_) => write!(f, "Execution failure"),
134            ACVMStatus::RequiresForeignCall(_) => write!(f, "Waiting on foreign call"),
135            ACVMStatus::RequiresAcirCall(_) => write!(f, "Waiting on acir call"),
136        }
137    }
138}
139
140#[expect(clippy::large_enum_variant)]
141pub enum StepResult<'a, F, B: BlackBoxFunctionSolver<F>> {
142    Status(ACVMStatus<F>),
143    IntoBrillig(BrilligSolver<'a, F, B>),
144}
145
146// This enum represents the different cases in which an
147// opcode can be unsolvable.
148// The most common being that one of its input has not been
149// assigned a value.
150//
151// TODO(https://github.com/noir-lang/noir/issues/10052): ExpressionHasTooManyUnknowns is specific for expression solver
152// TODO(https://github.com/noir-lang/noir/issues/10052): we could have a error enum for expression solver failure cases in that module
153// TODO(https://github.com/noir-lang/noir/issues/10052): that can be converted into an OpcodeNotSolvable or OpcodeResolutionError enum
154#[derive(Clone, PartialEq, Eq, Debug, Error)]
155pub enum OpcodeNotSolvable<F> {
156    #[error("missing assignment for witness index {0}")]
157    MissingAssignment(u32),
158    #[error("Attempted to load uninitialized memory block")]
159    MissingMemoryBlock(u32),
160    #[error("expression has too many unknowns {0}")]
161    ExpressionHasTooManyUnknowns(Expression<F>),
162}
163
164/// Used by errors to point to a specific opcode as that error's cause
165///
166/// Some errors don't have a specific opcode associated with them, or are created without one and added later.
167#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
168pub enum ErrorLocation {
169    #[default]
170    Unresolved,
171    Resolved(OpcodeLocation),
172}
173
174impl std::fmt::Display for ErrorLocation {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            ErrorLocation::Unresolved => write!(f, "unresolved"),
178            ErrorLocation::Resolved(location) => {
179                write!(f, "{location}")
180            }
181        }
182    }
183}
184
185/// A dynamic assertion payload whose data has been resolved.
186/// This is instantiated upon hitting an assertion failure.
187#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
188pub struct RawAssertionPayload<F> {
189    /// Selector to the respective ABI type the data in this payload represents
190    pub selector: ErrorSelector,
191    /// Resolved data that represents some ABI type.
192    /// To be decoded in the final step of error resolution.
193    pub data: Vec<F>,
194}
195
196/// Enumeration of possible resolved assertion payloads.
197/// This is instantiated upon hitting an assertion failure,
198/// and can either be static strings or dynamic payloads.
199#[derive(Clone, PartialEq, Eq, Debug)]
200pub enum ResolvedAssertionPayload<F> {
201    String(String),
202    Raw(RawAssertionPayload<F>),
203}
204
205#[derive(Clone, PartialEq, Eq, Debug, Error)]
206pub enum OpcodeResolutionError<F> {
207    #[error("Cannot solve opcode: {0}")]
208    OpcodeNotSolvable(#[from] OpcodeNotSolvable<F>),
209    #[error("Cannot satisfy constraint")]
210    UnsatisfiedConstrain {
211        opcode_location: ErrorLocation,
212        payload: Option<ResolvedAssertionPayload<F>>,
213    },
214    #[error("Index out of bounds, array has size {array_size:?}, but index was {index:?}")]
215    IndexOutOfBounds {
216        opcode_location: ErrorLocation,
217        index: F,
218        array_size: u32,
219        /// The message the failing memory op carries, when it has one.
220        ///
221        /// A memory op's bounds check stands in for an array access's out-of-bounds check, and the
222        /// index and size above are the flattened coordinates of ACIR memory. Where those are not
223        /// the coordinates the program was written in, ACIR generation attaches a payload that
224        /// describes the failure in the program's own terms.
225        payload: Option<ResolvedAssertionPayload<F>>,
226    },
227    #[error("Cannot solve opcode: {invalid_input_bit_size}")]
228    InvalidInputBitSize {
229        opcode_location: ErrorLocation,
230        invalid_input_bit_size: InvalidInputBitSize,
231    },
232    #[error("Failed to solve blackbox function: {0}, reason: {1}")]
233    BlackBoxFunctionFailed(BlackBoxFunc, String),
234    #[error("Failed to solve brillig function")]
235    BrilligFunctionFailed {
236        function_id: BrilligFunctionId,
237        call_stack: Vec<OpcodeLocation>,
238        payload: Option<ResolvedAssertionPayload<F>>,
239    },
240    #[error("Attempted to call `main` with a `Call` opcode")]
241    AcirMainCallAttempted { opcode_location: ErrorLocation },
242    #[error(
243        "{results_size:?} result values were provided for {outputs_size:?} call output witnesses, most likely due to bad ACIR codegen"
244    )]
245    AcirCallOutputsMismatch { opcode_location: ErrorLocation, results_size: u32, outputs_size: u32 },
246    #[error("Brillig function {function_id} returned data inconsistent with its call outputs")]
247    BrilligOutputsMismatch { function_id: BrilligFunctionId },
248    #[error("(--pedantic): Predicates are expected to be 0 or 1, but found: {pred_value}")]
249    PredicateLargerThanOne { opcode_location: ErrorLocation, pred_value: F },
250    #[error("(--pedantic): Memory operations are expected to be 0 or 1, but found: {operation}")]
251    MemoryOperationLargerThanOne { opcode_location: ErrorLocation, operation: F },
252}
253
254impl<F> From<BlackBoxResolutionError> for OpcodeResolutionError<F> {
255    fn from(value: BlackBoxResolutionError) -> Self {
256        match value {
257            BlackBoxResolutionError::Failed(func, reason) => {
258                OpcodeResolutionError::BlackBoxFunctionFailed(func, reason)
259            }
260            BlackBoxResolutionError::AssertFailed(error) => {
261                OpcodeResolutionError::UnsatisfiedConstrain {
262                    opcode_location: ErrorLocation::Unresolved,
263                    payload: Some(ResolvedAssertionPayload::String(error)),
264                }
265            }
266        }
267    }
268}
269
270impl<F> From<InvalidInputBitSize> for OpcodeResolutionError<F> {
271    fn from(invalid_input_bit_size: InvalidInputBitSize) -> Self {
272        Self::InvalidInputBitSize {
273            opcode_location: ErrorLocation::Unresolved,
274            invalid_input_bit_size,
275        }
276    }
277}
278
279pub type ProfilingSamples = Vec<ProfilingSample>;
280
281#[derive(Default)]
282pub struct ProfilingSample {
283    pub call_stack: Vec<OpcodeLocation>,
284    pub brillig_function_id: Option<BrilligFunctionId>,
285}
286
287pub struct ACVM<'a, F: AcirField, B: BlackBoxFunctionSolver<F>> {
288    status: ACVMStatus<F>,
289
290    backend: &'a B,
291
292    /// Stores the solver for memory operations acting on blocks of memory disambiguated by [block][`BlockId`].
293    block_solvers: HashMap<BlockId, MemoryOpSolver<F>>,
294
295    /// A list of opcodes which are to be executed by the ACVM.
296    opcodes: &'a [Opcode<F>],
297    /// Index of the next opcode to be executed.
298    instruction_pointer: usize,
299
300    /// A mapping of witnesses to their solved values
301    /// The map is updated as the ACVM executes.
302    witness_map: WitnessMap<F>,
303
304    brillig_solver: Option<BrilligSolver<'a, F, B>>,
305
306    /// A counter maintained throughout an ACVM process that determines
307    /// whether the caller has resolved the results of an ACIR [call][Opcode::Call].
308    acir_call_counter: usize,
309    /// Represents the outputs of all ACIR calls during an ACVM process
310    /// List is appended onto by the caller upon reaching a [`ACVMStatus::RequiresAcirCall`]
311    acir_call_results: Vec<Vec<F>>,
312
313    // Each unconstrained function referenced in the program
314    unconstrained_functions: &'a [BrilligBytecode<F>],
315
316    assertion_payloads: &'a [(OpcodeLocation, AssertionPayload<F>)],
317
318    profiling_active: bool,
319
320    profiling_samples: ProfilingSamples,
321
322    // Whether we need to trace brillig execution for fuzzing
323    brillig_fuzzing_active: bool,
324
325    // Brillig branch to feature map
326    brillig_branch_to_feature_map: Option<&'a BranchToFeatureMap>,
327
328    brillig_fuzzing_trace: Option<Vec<u32>>,
329}
330
331impl<'a, F: AcirField, B: BlackBoxFunctionSolver<F>> ACVM<'a, F, B> {
332    pub fn new(
333        backend: &'a B,
334        opcodes: &'a [Opcode<F>],
335        initial_witness: WitnessMap<F>,
336        unconstrained_functions: &'a [BrilligBytecode<F>],
337        assertion_payloads: &'a [(OpcodeLocation, AssertionPayload<F>)],
338    ) -> Self {
339        let status = if opcodes.is_empty() { ACVMStatus::Solved } else { ACVMStatus::InProgress };
340        ACVM {
341            status,
342            backend,
343            block_solvers: HashMap::default(),
344            opcodes,
345            instruction_pointer: 0,
346            witness_map: initial_witness,
347            brillig_solver: None,
348            acir_call_counter: 0,
349            acir_call_results: Vec::default(),
350            unconstrained_functions,
351            assertion_payloads,
352            profiling_active: false,
353            profiling_samples: Vec::new(),
354            brillig_fuzzing_active: false,
355            brillig_branch_to_feature_map: None,
356            brillig_fuzzing_trace: None,
357        }
358    }
359
360    /// Enable profiling
361    pub fn with_profiler(&mut self, profiling_active: bool) {
362        self.profiling_active = profiling_active;
363    }
364
365    /// Enable brillig fuzzing
366    pub fn with_brillig_fuzzing(
367        &mut self,
368        brillig_branch_to_feature_map: Option<&'a BranchToFeatureMap>,
369    ) {
370        self.brillig_fuzzing_active = brillig_branch_to_feature_map.is_some();
371        self.brillig_branch_to_feature_map = brillig_branch_to_feature_map;
372    }
373
374    pub fn get_brillig_fuzzing_trace(&self) -> Option<Vec<u32>> {
375        self.brillig_fuzzing_trace.clone()
376    }
377
378    /// Returns a reference to the current state of the ACVM's [`WitnessMap`].
379    ///
380    /// Once execution has completed, the witness map can be extracted using [`ACVM::finalize`]
381    pub fn witness_map(&self) -> &WitnessMap<F> {
382        &self.witness_map
383    }
384
385    pub fn overwrite_witness(&mut self, witness: Witness, value: F) -> Option<F> {
386        self.witness_map.insert(witness, value)
387    }
388
389    /// Returns a slice containing the opcodes of the circuit being executed.
390    pub fn opcodes(&self) -> &[Opcode<F>] {
391        self.opcodes
392    }
393
394    /// Returns the index of the current opcode to be executed.
395    pub fn instruction_pointer(&self) -> usize {
396        self.instruction_pointer
397    }
398
399    pub fn take_profiling_samples(&mut self) -> ProfilingSamples {
400        std::mem::take(&mut self.profiling_samples)
401    }
402
403    /// Finalize the ACVM execution, returning the resulting [`WitnessMap`].
404    pub fn finalize(self) -> WitnessMap<F> {
405        if self.status != ACVMStatus::Solved {
406            panic!("ACVM execution is not complete: ({})", self.status);
407        }
408        self.witness_map
409    }
410
411    /// Updates the current status of the VM.
412    /// Returns the given status.
413    fn status(&mut self, status: ACVMStatus<F>) -> ACVMStatus<F> {
414        self.status = status.clone();
415        status
416    }
417
418    pub fn get_status(&self) -> &ACVMStatus<F> {
419        &self.status
420    }
421
422    /// Sets the VM status to [`ACVMStatus::Failure`] using the provided `error`.
423    /// Returns the new status.
424    fn fail(&mut self, error: OpcodeResolutionError<F>) -> ACVMStatus<F> {
425        self.status(ACVMStatus::Failure(error))
426    }
427
428    /// Sets the status of the VM to `RequiresForeignCall`.
429    /// Indicating that the VM is now waiting for a foreign call to be resolved.
430    fn wait_for_foreign_call(&mut self, foreign_call: ForeignCallWaitInfo<F>) -> ACVMStatus<F> {
431        self.status(ACVMStatus::RequiresForeignCall(foreign_call))
432    }
433
434    /// Return a reference to the arguments for the next pending foreign call, if one exists.
435    pub fn get_pending_foreign_call(&self) -> Option<&ForeignCallWaitInfo<F>> {
436        if let ACVMStatus::RequiresForeignCall(foreign_call) = &self.status {
437            Some(foreign_call)
438        } else {
439            None
440        }
441    }
442
443    /// Resolves a foreign call's [result][brillig_vm::brillig::ForeignCallResult] using a result calculated outside of the ACVM.
444    ///
445    /// The ACVM can then be restarted to solve the remaining Brillig VM process as well as the remaining ACIR opcodes.
446    pub fn resolve_pending_foreign_call(&mut self, foreign_call_result: ForeignCallResult<F>) {
447        if !matches!(self.status, ACVMStatus::RequiresForeignCall(_)) {
448            panic!("ACVM is not expecting a foreign call response as no call was made");
449        }
450
451        let brillig_solver = self.brillig_solver.as_mut().expect("No active Brillig solver");
452        brillig_solver.resolve_pending_foreign_call(foreign_call_result);
453
454        // Now that the foreign call has been resolved then we can resume execution.
455        self.status(ACVMStatus::InProgress);
456    }
457
458    /// Sets the status of the VM to `RequiresAcirCall`
459    /// Indicating that the VM is now waiting for an ACIR call to be resolved
460    fn wait_for_acir_call(&mut self, acir_call: AcirCallWaitInfo<F>) -> ACVMStatus<F> {
461        self.status(ACVMStatus::RequiresAcirCall(acir_call))
462    }
463
464    /// Resolves an ACIR call's result (simply a list of fields) using a result calculated by a separate ACVM instance.
465    ///
466    /// The current ACVM instance can then be restarted to solve the remaining ACIR opcodes.
467    pub fn resolve_pending_acir_call(&mut self, call_result: Vec<F>) {
468        if !matches!(self.status, ACVMStatus::RequiresAcirCall(_)) {
469            panic!("ACVM is not expecting an ACIR call response as no call was made");
470        }
471
472        if self.acir_call_counter < self.acir_call_results.len() {
473            panic!("No unresolved ACIR calls");
474        }
475        self.acir_call_results.push(call_result);
476
477        // Now that the ACIR call has been resolved then we can resume execution.
478        self.status(ACVMStatus::InProgress);
479    }
480
481    /// Executes the ACVM's circuit until execution halts.
482    ///
483    /// Execution can halt due to three reasons:
484    /// 1. All opcodes have been executed successfully.
485    /// 2. The circuit has been found to be unsatisfiable.
486    /// 2. A Brillig [foreign call][`ForeignCallWaitInfo`] has been encountered and must be resolved.
487    pub fn solve(&mut self) -> ACVMStatus<F> {
488        while self.status == ACVMStatus::InProgress {
489            self.solve_opcode();
490        }
491        self.status.clone()
492    }
493
494    fn current_opcode(&self) -> &'a Opcode<F> {
495        &self.opcodes[self.instruction_pointer]
496    }
497
498    /// Executes a single opcode using the dedicated solver.
499    ///
500    /// Foreign or ACIR Calls are deferred to the caller, which will
501    /// either instantiate a new ACVM to execute the called ACIR function
502    /// or a custom implementation to execute the foreign call.
503    /// Then it will resume execution of the current ACVM with the results of the call.
504    pub fn solve_opcode(&mut self) -> ACVMStatus<F> {
505        let resolution = match self.current_opcode() {
506            Opcode::AssertZero(expr) => ExpressionSolver::solve(&mut self.witness_map, expr),
507            Opcode::BlackBoxFuncCall(bb_func) => {
508                blackbox::solve(self.backend, &mut self.witness_map, bb_func)
509            }
510            Opcode::MemoryInit { block_id, init, .. } => {
511                self.solve_memory_init_opcode(*block_id, init)
512            }
513            Opcode::MemoryOp { block_id, op } => match self.block_solvers.get_mut(block_id) {
514                Some(solver) => solver.solve_memory_op(op, &mut self.witness_map),
515                None => Err(OpcodeResolutionError::OpcodeNotSolvable(
516                    OpcodeNotSolvable::MissingMemoryBlock(block_id.as_u32()),
517                )),
518            },
519            Opcode::BrilligCall { id, inputs, outputs, predicate } => {
520                match self.solve_brillig_call_opcode(id, inputs, outputs, predicate) {
521                    Ok(Some(foreign_call)) => return self.wait_for_foreign_call(foreign_call),
522                    res => res.map(|_| ()),
523                }
524            }
525            Opcode::Call { id, inputs, outputs, predicate } => {
526                match self.solve_call_opcode(id, inputs, outputs, predicate) {
527                    Ok(Some(input_values)) => return self.wait_for_acir_call(input_values),
528                    res => res.map(|_| ()),
529                }
530            }
531        };
532        self.handle_opcode_resolution(resolution)
533    }
534
535    /// Returns the status of the ACVM
536    /// If the status is an error, it converts the error into [`OpcodeResolutionError`]
537    fn handle_opcode_resolution(
538        &mut self,
539        resolution: Result<(), OpcodeResolutionError<F>>,
540    ) -> ACVMStatus<F> {
541        match resolution {
542            Ok(()) => {
543                self.instruction_pointer += 1;
544                if self.instruction_pointer == self.opcodes.len() {
545                    self.status(ACVMStatus::Solved)
546                } else {
547                    self.status(ACVMStatus::InProgress)
548                }
549            }
550            Err(mut error) => {
551                match &mut error {
552                    // If we have an index out of bounds, unsatisfied constraint, or an invalid input bit size,
553                    // the opcode label will be unresolved because the solvers do not have knowledge of this information.
554                    // We resolve, by setting this to the corresponding opcode that we just attempted to solve.
555                    OpcodeResolutionError::IndexOutOfBounds {
556                        opcode_location: opcode_index,
557                        payload: assertion_payload,
558                        ..
559                    } => {
560                        let location = OpcodeLocation::Acir(self.instruction_pointer());
561                        *opcode_index = ErrorLocation::Resolved(location);
562                        *assertion_payload = self.extract_assertion_payload(location);
563                    }
564                    OpcodeResolutionError::UnsatisfiedConstrain {
565                        opcode_location: opcode_index,
566                        payload: assertion_payload,
567                    } => {
568                        let location = OpcodeLocation::Acir(self.instruction_pointer());
569                        *opcode_index = ErrorLocation::Resolved(location);
570                        *assertion_payload = self.extract_assertion_payload(location);
571                    }
572                    OpcodeResolutionError::InvalidInputBitSize {
573                        opcode_location: opcode_index,
574                        ..
575                    } => {
576                        let location = OpcodeLocation::Acir(self.instruction_pointer());
577                        *opcode_index = ErrorLocation::Resolved(location);
578                    }
579                    // All other errors are thrown normally.
580                    _ => (),
581                }
582                self.fail(error)
583            }
584        }
585    }
586
587    fn extract_assertion_payload(
588        &self,
589        location: OpcodeLocation,
590    ) -> Option<ResolvedAssertionPayload<F>> {
591        let (_, assertion_descriptor) =
592            self.assertion_payloads.iter().find(|(loc, _)| location == *loc)?;
593        let mut fields = Vec::new();
594        for expr in &assertion_descriptor.payload {
595            match expr {
596                ExpressionOrMemory::Expression(expr) => {
597                    let value = get_value(expr, &self.witness_map).ok()?;
598                    fields.push(value);
599                }
600                ExpressionOrMemory::Memory(block_id) => {
601                    let memory_block = self.block_solvers.get(block_id)?;
602                    fields.extend(&memory_block.block_value);
603                }
604            }
605        }
606        let error_selector = ErrorSelector::new(assertion_descriptor.error_selector);
607
608        Some(ResolvedAssertionPayload::Raw(RawAssertionPayload {
609            selector: error_selector,
610            data: fields,
611        }))
612    }
613
614    /// Initializes a memory block with values loaded from the witness map.
615    ///
616    /// Fails if the block has already been initialized.
617    fn solve_memory_init_opcode(
618        &mut self,
619        block_id: BlockId,
620        init: &[Witness],
621    ) -> Result<(), OpcodeResolutionError<F>> {
622        let solver = MemoryOpSolver::new(init, &self.witness_map)?;
623        if self.block_solvers.insert(block_id, solver).is_some() {
624            return Err(OpcodeResolutionError::UnsatisfiedConstrain {
625                opcode_location: ErrorLocation::Unresolved,
626                payload: None,
627            });
628        }
629        Ok(())
630    }
631
632    /// Solves a Brillig Call opcode, which represents a call to an unconstrained function.
633    /// It first handles the predicate and returns zero values if the predicate is false.
634    /// Then it executes (or resumes execution) the Brillig function using a Brillig VM.
635    fn solve_brillig_call_opcode(
636        &mut self,
637        id: &BrilligFunctionId,
638        inputs: &'a [BrilligInputs<F>],
639        outputs: &[BrilligOutputs],
640        predicate: &Expression<F>,
641    ) -> Result<Option<ForeignCallWaitInfo<F>>, OpcodeResolutionError<F>> {
642        let opcode_location =
643            ErrorLocation::Resolved(OpcodeLocation::Acir(self.instruction_pointer()));
644        if id.as_usize() >= self.unconstrained_functions.len() {
645            return Err(OpcodeResolutionError::BrilligFunctionFailed {
646                function_id: *id,
647                call_stack: vec![OpcodeLocation::Acir(self.instruction_pointer())],
648                payload: None,
649            });
650        }
651        if is_predicate_false(&self.witness_map, predicate, &opcode_location)? {
652            return BrilligSolver::<F, B>::zero_out_brillig_outputs(&mut self.witness_map, outputs)
653                .map(|_| None);
654        }
655
656        // If we're resuming execution after resolving a foreign call then
657        // there will be a cached `BrilligSolver` to avoid recomputation.
658        let mut solver: BrilligSolver<'_, F, B> = match self.brillig_solver.take() {
659            Some(solver) => solver,
660            None => BrilligSolver::new_call(
661                &self.witness_map,
662                &self.block_solvers,
663                inputs,
664                &self.unconstrained_functions[id.as_usize()].bytecode,
665                self.backend,
666                self.instruction_pointer,
667                *id,
668                self.profiling_active,
669                self.brillig_branch_to_feature_map,
670            )?,
671        };
672
673        // If we're fuzzing, we need to get the fuzzing trace on an error
674        let result = solver.solve().inspect_err(|_| {
675            if self.brillig_fuzzing_active {
676                self.brillig_fuzzing_trace = Some(solver.get_fuzzing_trace());
677            }
678        })?;
679
680        match result {
681            BrilligSolverStatus::ForeignCallWait(foreign_call) => {
682                // Cache the current state of the solver
683                self.brillig_solver = Some(solver);
684                Ok(Some(foreign_call))
685            }
686            BrilligSolverStatus::InProgress => {
687                unreachable!("Brillig solver still in progress")
688            }
689            BrilligSolverStatus::Finished => {
690                if self.brillig_fuzzing_active {
691                    self.brillig_fuzzing_trace = Some(solver.get_fuzzing_trace());
692                }
693                // Write execution outputs
694                if self.profiling_active {
695                    let profiling_info =
696                        solver.finalize_with_profiling(&mut self.witness_map, outputs)?;
697                    profiling_info.into_iter().for_each(|sample| {
698                        let mapped =
699                            sample.call_stack.into_iter().map(|loc| OpcodeLocation::Brillig {
700                                acir_index: self.instruction_pointer,
701                                brillig_index: loc,
702                            });
703                        self.profiling_samples.push(ProfilingSample {
704                            call_stack: std::iter::once(OpcodeLocation::Acir(
705                                self.instruction_pointer,
706                            ))
707                            .chain(mapped)
708                            .collect(),
709                            brillig_function_id: Some(*id),
710                        });
711                    });
712                } else {
713                    solver.finalize(&mut self.witness_map, outputs)?;
714                }
715
716                Ok(None)
717            }
718        }
719    }
720
721    // This function is used by the debugger
722    pub fn step_into_brillig(&mut self) -> StepResult<'a, F, B> {
723        let Opcode::BrilligCall { id, inputs, outputs, predicate } = self.current_opcode() else {
724            return StepResult::Status(self.solve_opcode());
725        };
726
727        let opcode_location =
728            ErrorLocation::Resolved(OpcodeLocation::Acir(self.instruction_pointer()));
729        if id.as_usize() >= self.unconstrained_functions.len() {
730            return StepResult::Status(self.handle_opcode_resolution(Err(
731                OpcodeResolutionError::BrilligFunctionFailed {
732                    function_id: *id,
733                    call_stack: vec![OpcodeLocation::Acir(self.instruction_pointer())],
734                    payload: None,
735                },
736            )));
737        }
738        let witness = &mut self.witness_map;
739        let should_skip = match is_predicate_false(witness, predicate, &opcode_location) {
740            Ok(result) => result,
741            Err(err) => return StepResult::Status(self.handle_opcode_resolution(Err(err))),
742        };
743        if should_skip {
744            let resolution = BrilligSolver::<F, B>::zero_out_brillig_outputs(witness, outputs);
745            return StepResult::Status(self.handle_opcode_resolution(resolution));
746        }
747
748        let solver = BrilligSolver::new_call(
749            witness,
750            &self.block_solvers,
751            inputs,
752            &self.unconstrained_functions[id.as_usize()].bytecode,
753            self.backend,
754            self.instruction_pointer,
755            *id,
756            self.profiling_active,
757            self.brillig_branch_to_feature_map,
758        );
759        match solver {
760            Ok(solver) => StepResult::IntoBrillig(solver),
761            Err(..) => StepResult::Status(self.handle_opcode_resolution(solver.map(|_| ()))),
762        }
763    }
764
765    // This function is used by the debugger
766    pub fn finish_brillig_with_solver(&mut self, solver: BrilligSolver<'a, F, B>) -> ACVMStatus<F> {
767        if !matches!(self.current_opcode(), Opcode::BrilligCall { .. }) {
768            unreachable!("Not executing a Brillig/BrilligCall opcode");
769        }
770        self.brillig_solver = Some(solver);
771        self.solve_opcode()
772    }
773
774    /// Defer execution of the ACIR call opcode to the caller, or finalize the execution.
775    /// 1. It first handles the predicate and return zero values if the predicate is false.
776    /// 2. If the results of the execution are not available, it issues a '`AcirCallWaitInfo`'
777    ///    to notify the caller that it (the caller) needs to execute the ACIR function.
778    /// 3. If the results are available, it updates the witness map and indicates that the opcode is solved.
779    pub fn solve_call_opcode(
780        &mut self,
781        id: &AcirFunctionId,
782        inputs: &[Witness],
783        outputs: &[Witness],
784        predicate: &Expression<F>,
785    ) -> Result<Option<AcirCallWaitInfo<F>>, OpcodeResolutionError<F>> {
786        let opcode_location =
787            ErrorLocation::Resolved(OpcodeLocation::Acir(self.instruction_pointer()));
788        if *id == AcirFunctionId::new(0) {
789            return Err(OpcodeResolutionError::AcirMainCallAttempted { opcode_location });
790        }
791
792        if is_predicate_false(&self.witness_map, predicate, &opcode_location)? {
793            // Zero out the outputs if we have a false predicate
794            for output in outputs {
795                insert_value(output, F::zero(), &mut self.witness_map)?;
796            }
797            return Ok(None);
798        }
799
800        if self.acir_call_counter >= self.acir_call_results.len() {
801            let mut initial_witness = WitnessMap::default();
802            for (i, input_witness) in inputs.iter().enumerate() {
803                let input_value = *witness_to_value(&self.witness_map, *input_witness)?;
804                initial_witness.insert(Witness::new(i as u32), input_value);
805            }
806            return Ok(Some(AcirCallWaitInfo { id: *id, initial_witness }));
807        }
808
809        let result_values = &self.acir_call_results[self.acir_call_counter];
810        if outputs.len() != result_values.len() {
811            return Err(OpcodeResolutionError::AcirCallOutputsMismatch {
812                opcode_location,
813                results_size: result_values.len() as u32,
814                outputs_size: outputs.len() as u32,
815            });
816        }
817
818        for (output_witness, result_value) in outputs.iter().zip_eq(result_values) {
819            insert_value(output_witness, *result_value, &mut self.witness_map)?;
820        }
821
822        self.acir_call_counter += 1;
823        Ok(None)
824    }
825}
826
827// Returns the concrete value for a particular witness
828// If the witness has no assignment, then
829// an error is returned
830pub fn witness_to_value<F>(
831    initial_witness: &WitnessMap<F>,
832    witness: Witness,
833) -> Result<&F, OpcodeResolutionError<F>> {
834    match initial_witness.get(&witness) {
835        Some(value) => Ok(value),
836        None => Err(OpcodeNotSolvable::MissingAssignment(witness.witness_index()).into()),
837    }
838}
839
840pub fn input_to_value<F: AcirField>(
841    initial_witness: &WitnessMap<F>,
842    input: FunctionInput<F>,
843) -> Result<F, OpcodeResolutionError<F>> {
844    match input {
845        FunctionInput::Witness(witness) => {
846            let initial_value = *witness_to_value(initial_witness, witness)?;
847            Ok(initial_value)
848        }
849        FunctionInput::Constant(value) => Ok(value),
850    }
851}
852
853pub fn check_bit_size<F: AcirField>(
854    value: F,
855    num_bits: u32,
856) -> Result<(), OpcodeResolutionError<F>> {
857    if value.num_bits() <= num_bits {
858        Ok(())
859    } else {
860        let value_num_bits = value.num_bits();
861        let value = value.to_string();
862        Err(OpcodeResolutionError::InvalidInputBitSize {
863            opcode_location: ErrorLocation::Unresolved,
864            invalid_input_bit_size: InvalidInputBitSize {
865                value,
866                value_num_bits,
867                max_bits: num_bits,
868            },
869        })
870    }
871}
872
873/// Returns the concrete value for a particular expression
874/// If the value cannot be computed, it returns an '`OpcodeNotSolvable`' error.
875pub fn get_value<F: AcirField>(
876    expr: &Expression<F>,
877    initial_witness: &WitnessMap<F>,
878) -> Result<F, OpcodeResolutionError<F>> {
879    if let Some(&c) = expr.to_const() {
880        return Ok(c);
881    }
882    let expr = ExpressionSolver::evaluate(expr, initial_witness);
883    match expr.to_const() {
884        Some(value) => Ok(*value),
885        None => Err(OpcodeResolutionError::OpcodeNotSolvable(
886            OpcodeNotSolvable::MissingAssignment(any_witness_from_expression(&expr).unwrap().0),
887        )),
888    }
889}
890
891/// Inserts `value` into the initial witness map under the index `witness`.
892///
893/// Returns an error if there was already a value in the map
894/// which does not match the value that one is about to insert
895pub fn insert_value<F: AcirField>(
896    witness: &Witness,
897    value_to_insert: F,
898    initial_witness: &mut WitnessMap<F>,
899) -> Result<(), OpcodeResolutionError<F>> {
900    use std::collections::btree_map::Entry;
901    match initial_witness.entry(*witness) {
902        Entry::Vacant(e) => {
903            e.insert(value_to_insert);
904            Ok(())
905        }
906        Entry::Occupied(e) => {
907            if *e.get() != value_to_insert {
908                Err(OpcodeResolutionError::UnsatisfiedConstrain {
909                    opcode_location: ErrorLocation::Unresolved,
910                    payload: None,
911                })
912            } else {
913                Ok(())
914            }
915        }
916    }
917}
918
919// Returns one witness belonging to an expression, in no relevant order
920// Returns None if the expression is const
921// The function is used during partial witness generation to report unsolved witness
922fn any_witness_from_expression<F>(expr: &Expression<F>) -> Option<Witness> {
923    if expr.linear_combinations.is_empty() {
924        if expr.mul_terms.is_empty() { None } else { Some(expr.mul_terms[0].1) }
925    } else {
926        Some(expr.linear_combinations[0].1)
927    }
928}
929
930/// Returns `Ok(true)` if the predicate is zero
931/// A predicate is used to indicate whether we should skip a certain operation.
932/// If we have a zero predicate it means the operation should be skipped.
933pub(crate) fn is_predicate_false<F: AcirField>(
934    witness: &WitnessMap<F>,
935    predicate: &Expression<F>,
936    opcode_location: &ErrorLocation,
937) -> Result<bool, OpcodeResolutionError<F>> {
938    let pred_value = get_value(predicate, witness)?;
939    let predicate_is_false = pred_value.is_zero();
940
941    // We expect that the predicate should resolve to either 0 or 1.
942    if !predicate_is_false && !pred_value.is_one() {
943        let opcode_location = *opcode_location;
944        return Err(OpcodeResolutionError::PredicateLargerThanOne { opcode_location, pred_value });
945    }
946
947    Ok(predicate_is_false)
948}
949
950/// Encapsulates a request from the ACVM that encounters an [ACIR call opcode][brillig_vm::brillig::Opcode::Call]
951/// where the result of the circuit execution has not yet been provided.
952///
953/// The caller must resolve this opcode externally based upon the information in the request.
954#[derive(Debug, Clone, PartialEq)]
955pub struct AcirCallWaitInfo<F> {
956    /// Index in the list of ACIR function's that should be called
957    pub id: AcirFunctionId,
958    /// Initial witness for the given circuit to be called
959    pub initial_witness: WitnessMap<F>,
960}
961
962#[cfg(test)]
963mod tests {
964    use std::collections::BTreeMap;
965
966    use acir::{
967        FieldElement,
968        native_types::{Witness, WitnessMap},
969        parse_opcodes,
970    };
971
972    use crate::pwg::{ACVM, ACVMStatus, OpcodeNotSolvable, OpcodeResolutionError};
973
974    #[test]
975    fn solve_simple_circuit() {
976        let initial_witness = WitnessMap::from(BTreeMap::from_iter([
977            (Witness(1), FieldElement::from(1u128)),
978            (Witness(2), FieldElement::from(1u128)),
979            (Witness(3), FieldElement::from(2u128)),
980        ]));
981        let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
982
983        let src = "
984        BLACKBOX::RANGE input: w1, bits: 32
985        BLACKBOX::RANGE input: w2, bits: 32
986        BLACKBOX::RANGE input: w3, bits: 32
987        ASSERT w4 = 2*w1 - w2
988        ASSERT w5 = -w2*w4 + 1
989        ";
990        let opcodes = parse_opcodes(src).unwrap();
991
992        let mut acvm = ACVM::new(&backend, &opcodes, initial_witness, &[], &[]);
993        assert_eq!(acvm.solve(), ACVMStatus::Solved);
994        assert_eq!(acvm.witness_map()[&Witness(5)], FieldElement::from(0u128));
995    }
996
997    #[test]
998    fn insert_value_does_not_overwrite_on_conflict() {
999        use crate::pwg::insert_value;
1000
1001        let old_value = FieldElement::from(1u128);
1002        let new_value = FieldElement::from(2u128);
1003        let witness = Witness(0);
1004
1005        let mut witness_map = WitnessMap::new();
1006        insert_value(&witness, old_value, &mut witness_map).expect("first insert should succeed");
1007
1008        let result = insert_value(&witness, new_value, &mut witness_map);
1009        assert!(
1010            matches!(result, Err(OpcodeResolutionError::UnsatisfiedConstrain { .. })),
1011            "expected UnsatisfiedConstrain error on conflicting insert"
1012        );
1013        assert_eq!(witness_map[&witness], old_value, "map should still hold the original value");
1014    }
1015
1016    #[test]
1017    fn errors_on_memory_op_without_init() {
1018        let initial_witness = WitnessMap::from(BTreeMap::from_iter([
1019            (Witness(0), FieldElement::from(0u128)),
1020            (Witness(1), FieldElement::from(0u128)),
1021        ]));
1022        let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
1023
1024        // READ against b0 without a prior `INIT b0 = ...` should error rather than panic.
1025        let src = "
1026        READ w1 = b0[w0]
1027        ";
1028        let opcodes = parse_opcodes(src).unwrap();
1029
1030        let mut acvm = ACVM::new(&backend, &opcodes, initial_witness, &[], &[]);
1031        let status = acvm.solve();
1032        assert!(
1033            matches!(
1034                status,
1035                ACVMStatus::Failure(OpcodeResolutionError::OpcodeNotSolvable(
1036                    OpcodeNotSolvable::MissingMemoryBlock(0)
1037                ))
1038            ),
1039            "expected MissingMemoryBlock(0) failure, got {status:?}",
1040        );
1041    }
1042
1043    #[test]
1044    fn errors_on_duplicate_memory_init() {
1045        let initial_witness = WitnessMap::from(BTreeMap::from_iter([
1046            (Witness(1), FieldElement::from(1u128)),
1047            (Witness(2), FieldElement::from(2u128)),
1048        ]));
1049        let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
1050
1051        let src = "
1052        INIT b0 = [w1, w2]
1053        INIT b0 = [w1, w2]
1054        ";
1055        let opcodes = parse_opcodes(src).unwrap();
1056
1057        let mut acvm = ACVM::new(&backend, &opcodes, initial_witness, &[], &[]);
1058        let status = acvm.solve();
1059        assert!(
1060            matches!(
1061                status,
1062                ACVMStatus::Failure(OpcodeResolutionError::UnsatisfiedConstrain { .. })
1063            ),
1064            "expected UnsatisfiedConstrain failure, got {status:?}",
1065        );
1066        assert_eq!(acvm.get_status(), &status, "status field should reflect the returned failure");
1067    }
1068
1069    #[test]
1070    fn errors_when_calling_function_zero() {
1071        let initial_witness =
1072            WitnessMap::from(BTreeMap::from_iter([(Witness(1), FieldElement::from(1u128))]));
1073        let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
1074
1075        let src = "
1076        CALL func: 0, predicate: 1, inputs: [w1], outputs: [w2]
1077        ";
1078        let opcodes = parse_opcodes(src).unwrap();
1079
1080        let mut acvm = ACVM::new(&backend, &opcodes, initial_witness, &[], &[]);
1081        assert!(matches!(
1082            acvm.solve(),
1083            ACVMStatus::Failure(OpcodeResolutionError::AcirMainCallAttempted { .. })
1084        ));
1085    }
1086
1087    mod brillig_oob {
1088        use std::collections::BTreeMap;
1089
1090        use acir::{
1091            FieldElement,
1092            circuit::{
1093                OpcodeLocation,
1094                brillig::{BrilligBytecode, BrilligFunctionId},
1095            },
1096            native_types::{Witness, WitnessMap},
1097            parse_opcodes,
1098        };
1099
1100        use crate::pwg::{ACVM, ACVMStatus, OpcodeResolutionError, StepResult};
1101        use test_case::test_case;
1102
1103        #[test_case(0, 1, 0 ; "empty function table")]
1104        #[test_case(3, 1, 1 ; "id past end of table")]
1105        #[test_case(5, 0, 0 ; "false predicate does not bypass check")]
1106        fn brillig_call_with_out_of_bounds_id_fails(
1107            func_id: u32,
1108            predicate: u32,
1109            table_size: usize,
1110        ) {
1111            let initial_witness =
1112                WitnessMap::from(BTreeMap::from_iter([(Witness(1), FieldElement::from(1u128))]));
1113            let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
1114
1115            let src = format!(
1116                "BRILLIG CALL func: {func_id}, predicate: {predicate}, inputs: [w1], outputs: [w2]"
1117            );
1118            let opcodes = parse_opcodes(&src).unwrap();
1119
1120            let unconstrained_functions =
1121                vec![
1122                    BrilligBytecode { function_name: "unused".to_string(), bytecode: vec![] };
1123                    table_size
1124                ];
1125
1126            let mut acvm =
1127                ACVM::new(&backend, &opcodes, initial_witness, &unconstrained_functions, &[]);
1128            assert_eq!(
1129                acvm.solve(),
1130                ACVMStatus::Failure(OpcodeResolutionError::BrilligFunctionFailed {
1131                    function_id: BrilligFunctionId::new(func_id),
1132                    call_stack: vec![OpcodeLocation::Acir(0)],
1133                    payload: None,
1134                }),
1135            );
1136        }
1137
1138        #[test]
1139        fn step_into_brillig_fails_on_out_of_bounds_id() {
1140            let initial_witness =
1141                WitnessMap::from(BTreeMap::from_iter([(Witness(1), FieldElement::from(1u128))]));
1142            let backend = acvm_blackbox_solver::StubbedBlackBoxSolver;
1143
1144            let src = "
1145            BRILLIG CALL func: 2, predicate: 1, inputs: [w1], outputs: [w2]
1146            ";
1147            let opcodes = parse_opcodes(src).unwrap();
1148
1149            let mut acvm = ACVM::new(&backend, &opcodes, initial_witness, &[], &[]);
1150            let step = acvm.step_into_brillig();
1151            match step {
1152                StepResult::Status(ACVMStatus::Failure(
1153                    OpcodeResolutionError::BrilligFunctionFailed {
1154                        function_id,
1155                        call_stack,
1156                        payload,
1157                    },
1158                )) => {
1159                    assert_eq!(function_id, BrilligFunctionId::new(2));
1160                    assert_eq!(call_stack, vec![OpcodeLocation::Acir(0)]);
1161                    assert!(payload.is_none());
1162                }
1163                StepResult::Status(other) => {
1164                    panic!("expected BrilligFunctionFailed, got status {other:?}")
1165                }
1166                StepResult::IntoBrillig(_) => {
1167                    panic!("expected BrilligFunctionFailed, stepped into Brillig instead")
1168                }
1169            }
1170        }
1171    }
1172}