brillig_vm/
lib.rs

1#![forbid(unsafe_code)]
2#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
3
4//! The Brillig VM is a specialized VM which allows the [ACVM][acvm] to perform custom non-determinism.
5//!
6//! Brillig bytecode is distinct from regular [ACIR][acir] in that it does not generate constraints.
7//!
8//! # Input Validation
9//!
10//! **Important:** The VM assumes that all inputs have been validated by the caller before execution.
11//! This includes ensuring that field element values fit within the expected bit sizes for typed
12//! operations (e.g., `u8`, `u16`, `u32`, etc.).
13//!
14//! If invalid inputs are provided, the VM may produce unexpected results without error:
15//! - Cast operations truncate call data values that exceed the target bit size (e.g., casting `256` to `u8` produces `0`)
16//!
17//! When using the VM with Noir programs, the ABI layer handles input validation
18//! automatically. Direct consumers of the VM API must implement their own input validation.
19//!
20//! [acir]: https://crates.io/crates/acir
21//! [acvm]: https://crates.io/crates/acvm
22
23use acir::AcirField;
24use acir::brillig::{
25    BinaryFieldOp, BinaryIntOp, ForeignCallParam, ForeignCallResult, IntegerBitSize, MemoryAddress,
26    Opcode,
27};
28use acvm_blackbox_solver::BlackBoxFunctionSolver;
29use arithmetic::{BrilligArithmeticError, evaluate_binary_field_op, evaluate_binary_int_op};
30use black_box::evaluate_black_box;
31
32// Re-export `brillig`.
33pub use acir::brillig;
34use memory::MemoryTypeError;
35pub use memory::{
36    FREE_MEMORY_POINTER_ADDRESS, MEMORY_ADDRESSING_BIT_SIZE, Memory, MemoryValue,
37    STACK_POINTER_ADDRESS, offsets,
38};
39
40pub use crate::fuzzing::BranchToFeatureMap;
41use crate::fuzzing::FuzzingTrace;
42
43mod arithmetic;
44mod black_box;
45mod cast;
46mod foreign_call;
47pub mod fuzzing;
48mod memory;
49
50/// Converts a u32 value to usize, panicking if the conversion fails.
51fn assert_usize(value: u32) -> usize {
52    value.try_into().expect("Failed conversion from u32 to usize")
53}
54
55/// Converts a usize value to u32, panicking if the conversion fails.
56fn assert_u32(value: usize) -> u32 {
57    value.try_into().expect("Failed conversion from usize to u32")
58}
59
60/// The error call stack contains the opcode indexes of the call stack at the time of failure, plus the index of the opcode that failed.
61pub type ErrorCallStack = Vec<usize>;
62
63/// Represents the reason why the Brillig VM failed during execution.
64#[derive(Debug, PartialEq, Eq, Clone)]
65pub enum FailureReason {
66    /// A trap was encountered, which indicates an explicit failure from within the VM program.
67    ///
68    /// A trap is triggered explicitly by the [trap opcode][Opcode::Trap].
69    /// The revert data is referenced by the offset and size in the VM memory.
70    Trap {
71        /// Offset in memory where the revert data begins.
72        revert_data_offset: u32,
73        /// Size of the revert data.
74        revert_data_size: u32,
75    },
76    /// A runtime failure during execution.
77    /// This error is triggered by all opcodes aside the [trap opcode][Opcode::Trap].
78    /// For example, a [binary operation][Opcode::BinaryIntOp] can trigger a division by zero error.
79    RuntimeError { message: String },
80}
81
82/// Represents the current execution status of the Brillig VM.
83#[derive(Debug, PartialEq, Eq, Clone)]
84pub enum VMStatus<F> {
85    /// The VM has completed execution successfully.
86    /// The output of the program is stored in the VM memory and can be accessed via the provided offset and size.
87    Finished {
88        /// Offset in memory where the return data begins.
89        return_data_offset: u32,
90        /// Size of the return data.
91        return_data_size: u32,
92    },
93    /// The VM is still in progress and has not yet completed execution.
94    /// This is used when simulating execution.
95    InProgress,
96    /// The VM encountered a failure and halted execution.
97    Failure {
98        /// The reason for the failure.
99        reason: FailureReason,
100        /// The call stack at the time the failure occurred, useful for debugging nested calls.
101        call_stack: ErrorCallStack,
102    },
103    /// The VM process is not solvable as a [foreign call][Opcode::ForeignCall] has been
104    /// reached where the outputs are yet to be resolved.
105    ///
106    /// The caller should interpret the information returned to compute a [ForeignCallResult]
107    /// and update the Brillig process. The VM can then be restarted to fully solve the previously
108    /// unresolved foreign call as well as the remaining Brillig opcodes.
109    ForeignCallWait {
110        /// Interpreted by simulator context.
111        function: String,
112        /// Input values.
113        /// Each input can be either a single value or an array of values read from a memory pointer.
114        inputs: Vec<ForeignCallParam<F>>,
115    },
116}
117
118/// The position of an opcode that is currently being executed in the bytecode.
119pub type OpcodePosition = usize;
120
121/// The position of the next opcode that will be executed in the bytecode,
122/// or an id of a specific state produced by the opcode.
123pub type NextOpcodePositionOrState = usize;
124
125/// A sample for an executed opcode.
126#[derive(Debug, PartialEq, Eq, Clone)]
127pub struct BrilligProfilingSample {
128    /// The call stack when processing a given opcode.
129    pub call_stack: Vec<usize>,
130}
131
132/// All samples for each opcode that was executed.
133pub type BrilligProfilingSamples = Vec<BrilligProfilingSample>;
134
135#[derive(Debug, PartialEq, Eq, Clone)]
136/// VM encapsulates the state of the Brillig VM during execution.
137pub struct VM<'a, F, B: BlackBoxFunctionSolver<F>> {
138    /// Calldata to the brillig function.
139    calldata: Vec<F>,
140    /// Instruction pointer.
141    program_counter: usize,
142    /// A counter maintained throughout a Brillig process that determines
143    /// whether the caller has resolved the results of a [foreign call][Opcode::ForeignCall].
144    ///
145    /// Incremented when the results of a foreign call have been processed and the output
146    /// values were written to memory.
147    ///
148    /// * When the counter is less than the length of the results, it indicates that we have
149    ///   unprocessed responses returned from the external foreign call handler.
150    foreign_call_counter: usize,
151    /// Accumulates the outputs of all foreign calls during a Brillig process.
152    /// The list is appended onto by the caller upon reaching a [VMStatus::ForeignCallWait].
153    foreign_call_results: Vec<ForeignCallResult<F>>,
154    /// Executable opcodes.
155    bytecode: &'a [Opcode<F>],
156    /// Status of the VM.
157    status: VMStatus<F>,
158    /// Memory of the VM.
159    memory: Memory<F>,
160    /// Call stack.
161    call_stack: Vec<usize>,
162    /// The solver for blackbox functions.
163    black_box_solver: &'a B,
164    // Flag that determines whether we want to profile VM.
165    profiling_active: bool,
166    // Samples for profiling the VM execution.
167    profiling_samples: BrilligProfilingSamples,
168
169    /// Fuzzing trace structure.
170    /// If the field is `None` then fuzzing is inactive.
171    fuzzing_trace: Option<FuzzingTrace>,
172}
173
174impl<'a, F: AcirField, B: BlackBoxFunctionSolver<F>> VM<'a, F, B> {
175    /// Constructs a new VM instance.
176    pub fn new(
177        calldata: Vec<F>,
178        bytecode: &'a [Opcode<F>],
179        black_box_solver: &'a B,
180        profiling_active: bool,
181        with_branch_to_feature_map: Option<&BranchToFeatureMap>,
182    ) -> Self {
183        let fuzzing_trace = with_branch_to_feature_map.cloned().map(FuzzingTrace::new);
184
185        Self {
186            calldata,
187            program_counter: 0,
188            foreign_call_counter: 0,
189            foreign_call_results: Vec::new(),
190            bytecode,
191            status: VMStatus::InProgress,
192            memory: Memory::default(),
193            call_stack: Vec::new(),
194            black_box_solver,
195            profiling_active,
196            profiling_samples: Vec::with_capacity(bytecode.len()),
197            fuzzing_trace,
198        }
199    }
200
201    pub fn is_profiling_active(&self) -> bool {
202        self.profiling_active
203    }
204
205    pub fn is_fuzzing_active(&self) -> bool {
206        self.fuzzing_trace.is_some()
207    }
208
209    pub fn take_profiling_samples(&mut self) -> BrilligProfilingSamples {
210        std::mem::take(&mut self.profiling_samples)
211    }
212
213    /// Updates the current status of the VM.
214    /// Returns the given status.
215    fn status(&mut self, status: VMStatus<F>) -> &VMStatus<F> {
216        self.status = status;
217        &self.status
218    }
219
220    pub fn get_status(&self) -> VMStatus<F> {
221        self.status.clone()
222    }
223
224    /// Sets the current status of the VM to Finished (completed execution).
225    fn finish(&mut self, return_data_offset: u32, return_data_size: u32) -> &VMStatus<F> {
226        self.status(VMStatus::Finished { return_data_offset, return_data_size })
227    }
228
229    /// Check whether the latest foreign call result is available yet.
230    fn has_unprocessed_foreign_call_result(&self) -> bool {
231        self.foreign_call_counter < self.foreign_call_results.len()
232    }
233
234    /// Provide the results of a Foreign Call to the VM
235    /// and resume execution of the VM.
236    pub fn resolve_foreign_call(&mut self, foreign_call_result: ForeignCallResult<F>) {
237        if self.has_unprocessed_foreign_call_result() {
238            panic!("No unresolved foreign calls; the previous results haven't been processed yet");
239        }
240        self.foreign_call_results.push(foreign_call_result);
241        self.status(VMStatus::InProgress);
242    }
243
244    /// Sets the current status of the VM to `Failure`,
245    /// indicating that the VM encountered a `Trap` Opcode.
246    fn trap(&mut self, revert_data_offset: u32, revert_data_size: u32) -> &VMStatus<F> {
247        self.status(VMStatus::Failure {
248            call_stack: self.get_call_stack(),
249            reason: FailureReason::Trap { revert_data_offset, revert_data_size },
250        })
251    }
252
253    /// Sets the current status of the VM to `Failure`,
254    /// indicating that the VM encountered an invalid state.
255    fn fail(&mut self, message: String) -> &VMStatus<F> {
256        self.status(VMStatus::Failure {
257            call_stack: self.get_call_stack(),
258            reason: FailureReason::RuntimeError { message },
259        })
260    }
261
262    /// Process opcodes in a loop until a status of `Finished`,
263    /// `Failure` or `ForeignCallWait` is encountered.
264    pub fn process_opcodes(&mut self) -> VMStatus<F> {
265        while !matches!(
266            self.process_opcode(),
267            VMStatus::Finished { .. } | VMStatus::Failure { .. } | VMStatus::ForeignCallWait { .. }
268        ) {}
269        self.status.clone()
270    }
271
272    /// Read memory slots.
273    ///
274    /// Used by the debugger to inspect the contents of the memory.
275    pub fn get_memory(&self) -> &[MemoryValue<F>] {
276        self.memory.values()
277    }
278
279    /// Take all the contents of the memory, leaving it empty.
280    ///
281    /// Used only for testing purposes.
282    pub fn take_memory(mut self) -> Memory<F> {
283        std::mem::take(&mut self.memory)
284    }
285
286    pub fn foreign_call_counter(&self) -> usize {
287        self.foreign_call_counter
288    }
289
290    /// Write a numeric value to direct memory slot.
291    ///
292    /// Used by the debugger to alter memory.
293    pub fn write_memory_at(&mut self, ptr: u32, value: MemoryValue<F>) {
294        self.memory.write(MemoryAddress::direct(ptr), value);
295    }
296
297    /// Returns the VM's current call stack, including the actual program
298    /// counter in the last position of the returned vector.
299    pub fn get_call_stack(&self) -> Vec<usize> {
300        let mut call_stack = self.get_call_stack_no_current_counter();
301        call_stack.push(self.program_counter);
302        call_stack
303    }
304
305    /// Returns the VM's call stack, but unlike [Self::get_call_stack] without the attaching
306    /// the program counter in the last position of the returned vector.
307    /// This is meant only for fetching the call stack after execution has completed.
308    pub fn get_call_stack_no_current_counter(&self) -> Vec<usize> {
309        self.call_stack.clone()
310    }
311
312    /// Process a single opcode and modify the program counter.
313    pub fn process_opcode(&mut self) -> &VMStatus<F> {
314        if self.profiling_active {
315            let call_stack: Vec<usize> = self.get_call_stack();
316            self.profiling_samples.push(BrilligProfilingSample { call_stack });
317        }
318
319        self.process_opcode_internal()
320    }
321
322    pub fn get_fuzzing_trace(&self) -> Vec<u32> {
323        self.fuzzing_trace.as_ref().map(|trace| trace.get_trace()).unwrap_or_default()
324    }
325
326    /// Execute a single opcode:
327    /// 1. Retrieve the current opcode using the program counter
328    /// 2. Execute the opcode.
329    ///    - For instance a binary 'result = lhs+rhs' opcode will read the VM memory at the 'lhs' and 'rhs' addresses,
330    ///      compute the sum and write it to the 'result' memory address.
331    /// 3. Update the program counter, usually by incrementing it.
332    ///
333    /// - Control flow opcodes jump around the bytecode by setting the program counter.
334    /// - Foreign call opcodes pause the VM until the foreign call results are available.
335    /// - Function call opcodes backup the current program counter into the call stack and jump to the function entry point.
336    ///   The stack frame for function calls is handled during codegen.
337    fn process_opcode_internal(&mut self) -> &VMStatus<F> {
338        let opcode = &self.bytecode[self.program_counter];
339        match opcode {
340            Opcode::BinaryFieldOp { op, lhs, rhs, destination: result } => {
341                if let Err(error) = self.process_binary_field_op(*op, *lhs, *rhs, *result) {
342                    self.fail(error.to_string())
343                } else {
344                    self.increment_program_counter()
345                }
346            }
347            Opcode::BinaryIntOp { op, bit_size, lhs, rhs, destination: result } => {
348                match self.process_free_memory_op(*op, *bit_size, *lhs, *rhs, *result) {
349                    Err(error) => return self.fail(error),
350                    Ok(true) => return self.increment_program_counter(),
351                    Ok(false) => {
352                        // Not a free memory op, carry on as a regular binary operation.
353                    }
354                }
355                if let Err(error) = self.process_binary_int_op(*op, *bit_size, *lhs, *rhs, *result)
356                {
357                    self.fail(error.to_string())
358                } else {
359                    self.increment_program_counter()
360                }
361            }
362            Opcode::Not { destination, source, bit_size } => {
363                if let Err(error) = self.process_not(*source, *destination, *bit_size) {
364                    self.fail(error.to_string())
365                } else {
366                    self.increment_program_counter()
367                }
368            }
369            Opcode::Cast { destination, source, bit_size } => {
370                let source_value = self.memory.read(*source);
371                let casted_value = cast::cast(source_value, *bit_size);
372                self.memory.write(*destination, casted_value);
373                self.increment_program_counter()
374            }
375            Opcode::Jump { location: destination } => self.set_program_counter(*destination),
376            Opcode::JumpIf { condition, location: destination } => {
377                // Check if condition is true
378                // We use 0 to mean false and any other value to mean true
379                let condition_value = self.memory.read(*condition);
380                let condition_value = match condition_value.expect_u1() {
381                    Err(error) => {
382                        return self.fail(format!("condition value is not a boolean: {error}"));
383                    }
384                    Ok(cond) => cond,
385                };
386                if condition_value {
387                    self.fuzzing_trace_branching(*destination);
388                    self.set_program_counter(*destination)
389                } else {
390                    self.fuzzing_trace_branching(self.program_counter + 1);
391                    self.increment_program_counter()
392                }
393            }
394            Opcode::CalldataCopy { destination_address, size_address, offset_address } => {
395                let size = assert_usize(self.memory.read(*size_address).to_u32());
396                let offset = assert_usize(self.memory.read(*offset_address).to_u32());
397                let values: Vec<_> = self.calldata[offset..(offset + size)]
398                    .iter()
399                    .map(|value| MemoryValue::new_field(*value))
400                    .collect();
401                self.memory.write_slice(*destination_address, &values);
402                self.increment_program_counter()
403            }
404            Opcode::Return => {
405                if let Some(return_location) = self.call_stack.pop() {
406                    self.set_program_counter(return_location + 1)
407                } else {
408                    self.fail("return opcode hit, but callstack already empty".to_string())
409                }
410            }
411            Opcode::ForeignCall {
412                function,
413                destinations,
414                destination_value_types,
415                inputs,
416                input_value_types,
417            } => self.process_foreign_call(
418                function,
419                destinations,
420                destination_value_types,
421                inputs,
422                input_value_types,
423            ),
424            Opcode::Mov { destination: destination_address, source: source_address } => {
425                let source_value = self.memory.read(*source_address);
426                self.memory.write(*destination_address, source_value);
427                self.increment_program_counter()
428            }
429            Opcode::ConditionalMov { destination, source_a, source_b, condition } => {
430                let condition_value = self.memory.read(*condition);
431
432                let condition_value = match condition_value.expect_u1() {
433                    Err(error) => {
434                        return self.fail(format!("condition value is not a boolean: {error}"));
435                    }
436                    Ok(cond) => cond,
437                };
438                if condition_value {
439                    self.memory.write(*destination, self.memory.read(*source_a));
440                } else {
441                    self.memory.write(*destination, self.memory.read(*source_b));
442                }
443                self.fuzzing_trace_conditional_mov(condition_value);
444                self.increment_program_counter()
445            }
446            Opcode::Trap { revert_data } => {
447                let revert_data_size = self.memory.read(revert_data.size).to_u32();
448                if revert_data_size > 0 {
449                    self.trap(
450                        self.memory.read_ref(revert_data.pointer).unwrap_direct(),
451                        revert_data_size,
452                    )
453                } else {
454                    self.trap(0, 0)
455                }
456            }
457            Opcode::Stop { return_data } => {
458                let return_data_size = self.memory.read(return_data.size).to_u32();
459                if return_data_size > 0 {
460                    self.finish(
461                        self.memory.read_ref(return_data.pointer).unwrap_direct(),
462                        return_data_size,
463                    )
464                } else {
465                    self.finish(0, 0)
466                }
467            }
468            Opcode::Load { destination, source_pointer } => {
469                // Convert the source_pointer to an address
470                let source = self.memory.read_ref(*source_pointer);
471                // Use the source address to lookup the value in memory
472                let value = self.memory.read(source);
473                self.memory.write(*destination, value);
474                self.increment_program_counter()
475            }
476            Opcode::Store { destination_pointer, source: source_address } => {
477                // Convert the destination_pointer to an address
478                let destination = self.memory.read_ref(*destination_pointer);
479                // Read the value at the source address
480                let value = self.memory.read(*source_address);
481                // Use the destination address to set the value in memory
482                self.memory.write(destination, value);
483                self.increment_program_counter()
484            }
485            Opcode::Call { location } => {
486                // Push the return location to the call stack.
487                self.call_stack.push(self.program_counter);
488                self.set_program_counter(*location)
489            }
490            Opcode::Const { destination, value, bit_size } => {
491                // Consts are not checked in runtime to fit in the bit size, since they can safely be checked statically.
492                self.memory.write(*destination, MemoryValue::new_from_field(*value, *bit_size));
493                self.increment_program_counter()
494            }
495            Opcode::IndirectConst { destination_pointer, bit_size, value } => {
496                // Convert the destination_pointer to an address
497                let destination = self.memory.read_ref(*destination_pointer);
498                // Use the destination address to set the value in memory
499                self.memory.write(destination, MemoryValue::new_from_field(*value, *bit_size));
500                self.increment_program_counter()
501            }
502            Opcode::BlackBox(black_box_op) => {
503                if let Err(e) =
504                    evaluate_black_box(black_box_op, self.black_box_solver, &mut self.memory)
505                {
506                    self.fail(e.to_string())
507                } else {
508                    self.increment_program_counter()
509                }
510            }
511        }
512    }
513
514    /// Returns the current value of the program counter.
515    pub fn program_counter(&self) -> usize {
516        self.program_counter
517    }
518
519    /// Increments the program counter by 1.
520    fn increment_program_counter(&mut self) -> &VMStatus<F> {
521        self.set_program_counter(self.program_counter + 1)
522    }
523
524    /// Sets the program counter to `value`.
525    /// If the program counter no longer points to an opcode
526    /// in the bytecode, then the VMStatus reports `Finished`.
527    fn set_program_counter(&mut self, value: usize) -> &VMStatus<F> {
528        assert!(self.program_counter < self.bytecode.len());
529        self.program_counter = value;
530        if self.program_counter >= self.bytecode.len() {
531            self.status = VMStatus::Finished { return_data_offset: 0, return_data_size: 0 };
532        }
533        &self.status
534    }
535
536    /// Process a binary field operation.
537    /// This method will not modify the program counter.
538    fn process_binary_field_op(
539        &mut self,
540        op: BinaryFieldOp,
541        lhs: MemoryAddress,
542        rhs: MemoryAddress,
543        result: MemoryAddress,
544    ) -> Result<(), BrilligArithmeticError> {
545        let lhs_value = self.memory.read(lhs);
546        let rhs_value = self.memory.read(rhs);
547
548        let result_value = evaluate_binary_field_op(&op, lhs_value, rhs_value)?;
549        self.memory.write(result, result_value);
550        self.fuzzing_trace_binary_field_op_comparison(&op, lhs_value, rhs_value, result_value);
551        Ok(())
552    }
553
554    /// Process a binary integer operation.
555    /// This method will not modify the program counter.
556    fn process_binary_int_op(
557        &mut self,
558        op: BinaryIntOp,
559        bit_size: IntegerBitSize,
560        lhs: MemoryAddress,
561        rhs: MemoryAddress,
562        result: MemoryAddress,
563    ) -> Result<(), BrilligArithmeticError> {
564        let lhs_value = self.memory.read(lhs);
565        let rhs_value = self.memory.read(rhs);
566
567        let result_value = evaluate_binary_int_op(&op, lhs_value, rhs_value, bit_size)?;
568        self.memory.write(result, result_value);
569        self.fuzzing_trace_binary_int_op_comparison(&op, lhs_value, rhs_value, result_value);
570        Ok(())
571    }
572
573    /// Special handling for the increment of the _free memory pointer_.
574    ///
575    /// Binary operations in Brillig wrap around on overflow,
576    /// but there are usually other instruction in the SSA itself
577    /// to make sure the circuit fails when overflows occur.
578    ///
579    /// This is not the case for the _free memory pointer_ itself, however,
580    /// which only exists in Brillig, and points at the first free memory
581    /// slot on the heap where nothing has been allocated yet. If we allowed
582    /// it to wrap around during an overflowing increment, then we could end
583    /// up overwriting parts of the memory reserved for globals, the stack,
584    /// or other values on the heap.
585    ///
586    /// This special handling is not adopted by the AVM. Still, if it fails
587    /// here, at least we have a way to detect this unlikely edge case,
588    /// rather than go into undefined behavior by corrupting memory.
589    /// Detecting overflows with additional bytecode would be an overkill.
590    /// Perhaps in the future the AVM will offer checked operations instead.
591    ///
592    /// Returns:
593    /// * `Ok(false)` if it's not a _free memory pointer_ increase
594    /// * `Ok(true)` if the operation was handled
595    /// * `Err(RuntimeError("Out of memory"))` if there was an overflow
596    fn process_free_memory_op(
597        &mut self,
598        op: BinaryIntOp,
599        bit_size: IntegerBitSize,
600        lhs: MemoryAddress,
601        rhs: MemoryAddress,
602        result: MemoryAddress,
603    ) -> Result<bool, String> {
604        if result != FREE_MEMORY_POINTER_ADDRESS
605            || op != BinaryIntOp::Add
606            || bit_size != MEMORY_ADDRESSING_BIT_SIZE
607        {
608            return Ok(false);
609        }
610
611        let lhs_value = self.memory.read(lhs);
612        let rhs_value = self.memory.read(rhs);
613
614        let MemoryValue::U32(lhs_value) = lhs_value else {
615            return Ok(false);
616        };
617        let MemoryValue::U32(rhs_value) = rhs_value else {
618            return Ok(false);
619        };
620        let Some(result_value) = lhs_value.checked_add(rhs_value) else {
621            return Err("Out of memory".to_string());
622        };
623
624        self.memory.write(result, result_value.into());
625
626        Ok(true)
627    }
628
629    /// Process a unary negation operation.
630    ///
631    /// It returns `MemoryTypeError` if the value type does not match the type
632    /// indicated by `op_bit_size`.
633    fn process_not(
634        &mut self,
635        source: MemoryAddress,
636        destination: MemoryAddress,
637        op_bit_size: IntegerBitSize,
638    ) -> Result<(), MemoryTypeError> {
639        let value = self.memory.read(source);
640
641        let negated_value = match op_bit_size {
642            IntegerBitSize::U1 => MemoryValue::U1(!value.expect_u1()?),
643            IntegerBitSize::U8 => MemoryValue::U8(!value.expect_u8()?),
644            IntegerBitSize::U16 => MemoryValue::U16(!value.expect_u16()?),
645            IntegerBitSize::U32 => MemoryValue::U32(!value.expect_u32()?),
646            IntegerBitSize::U64 => MemoryValue::U64(!value.expect_u64()?),
647            IntegerBitSize::U128 => MemoryValue::U128(!value.expect_u128()?),
648        };
649        self.memory.write(destination, negated_value);
650        Ok(())
651    }
652}