From f09306ae7571a0bd9f42b9e1f4b861b94f6ec16c Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Mon, 28 Jul 2025 18:09:00 +0200 Subject: [PATCH 1/2] vm core: implement run_instruction --- Cargo.toml | 1 + crates/leanVm/Cargo.toml | 1 + crates/leanVm/src/context/run_context.rs | 95 ++++++++++- crates/leanVm/src/core.rs | 208 +++++++++++++++++++++-- crates/leanVm/src/errors/math.rs | 8 +- crates/leanVm/src/errors/memory.rs | 13 +- crates/leanVm/src/errors/vm.rs | 15 +- crates/leanVm/src/memory/address.rs | 87 ++++++++-- crates/leanVm/src/memory/manager.rs | 4 +- crates/leanVm/src/memory/val.rs | 164 ++++++++++++++++++ 10 files changed, 564 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ad2b1d8..4dc8b46a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ p3-koala-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "d0c4a36 thiserror = "2.0" proptest = "1.7" +num-traits = "0.2" diff --git a/crates/leanVm/Cargo.toml b/crates/leanVm/Cargo.toml index 649e0683..1559403a 100644 --- a/crates/leanVm/Cargo.toml +++ b/crates/leanVm/Cargo.toml @@ -14,6 +14,7 @@ p3-koala-bear.workspace = true p3-field.workspace = true thiserror.workspace = true +num-traits.workspace = true [dev-dependencies] proptest.workspace = true diff --git a/crates/leanVm/src/context/run_context.rs b/crates/leanVm/src/context/run_context.rs index 71bcca8c..5ee89f54 100644 --- a/crates/leanVm/src/context/run_context.rs +++ b/crates/leanVm/src/context/run_context.rs @@ -3,7 +3,7 @@ use p3_field::PrimeField64; use crate::{ errors::{memory::MemoryError, vm::VirtualMachineError}, memory::{address::MemoryAddress, manager::MemoryManager, val::MemoryValue}, - types::instruction::MemOrConstant, + types::instruction::{MemOrConstant, MemOrFp, MemOrFpOrConstant}, }; #[derive(Debug, Default)] @@ -47,7 +47,56 @@ impl RunContext { match operand { MemOrConstant::Constant(val) => Ok(MemoryValue::Int(*val)), MemOrConstant::MemoryAfterFp { shift } => { - let addr = (self.fp + *shift)?; + let addr = self.fp.add_usize(*shift)?; + memory + .get(addr) + .ok_or_else(|| MemoryError::UninitializedMemory(addr).into()) + } + } + } + + /// Resolves a `MemOrFp` operand to its final value. + /// + /// - If the operand is the frame pointer `Fp`, it returns the `fp` address itself. + /// - If it's a memory location, it computes the address relative to `fp` and fetches the value. + pub fn get_value_from_mem_or_fp( + &self, + operand: &MemOrFp, + memory: &MemoryManager, + ) -> Result, VirtualMachineError> + where + F: PrimeField64, + { + match operand { + MemOrFp::Fp => Ok(MemoryValue::Address(self.fp)), + MemOrFp::MemoryAfterFp { shift } => { + let addr = self.fp.add_usize(*shift)?; + memory + .get(addr) + .ok_or_else(|| MemoryError::UninitializedMemory(addr).into()) + } + } + } + + /// Resolves a `MemOrFpOrConstant` operand to its final value. + /// + /// This is a comprehensive resolver that handles all three potential operand types: + /// - a constant value, + /// - a memory location relative to `fp`, + /// - the `fp` register itself. + pub fn get_value_from_mem_or_fp_or_constant( + &self, + operand: &MemOrFpOrConstant, + memory: &MemoryManager, + ) -> Result, VirtualMachineError> + where + F: PrimeField64, + { + match operand { + MemOrFpOrConstant::Constant(val) => Ok(MemoryValue::Int(*val)), + MemOrFpOrConstant::Fp => Ok(MemoryValue::Address(self.fp)), + MemOrFpOrConstant::MemoryAfterFp { shift } => { + let addr = self.fp.add_usize(*shift)?; memory .get(addr) .ok_or_else(|| MemoryError::UninitializedMemory(addr).into()) @@ -159,4 +208,46 @@ mod tests { other => panic!("Unexpected error: {other:?}"), } } + + #[test] + fn test_get_value_from_mem_or_fp_or_constant_is_constant() { + let ctx = RunContext::new(MemoryAddress::new(0, 0), MemoryAddress::new(1, 0)); + let operand = MemOrFpOrConstant::Constant(F::from_u64(123)); + let memory = MemoryManager::default(); + let result = ctx + .get_value_from_mem_or_fp_or_constant(&operand, &memory) + .unwrap(); + assert_eq!(result, MemoryValue::Int(F::from_u64(123))); + } + + #[test] + fn test_get_value_from_mem_or_fp_or_constant_is_fp() { + let fp_addr = MemoryAddress::new(1, 10); + let ctx = RunContext::new(MemoryAddress::new(0, 0), fp_addr); + let operand = MemOrFpOrConstant::::Fp; + let memory = MemoryManager::default(); + let result = ctx + .get_value_from_mem_or_fp_or_constant(&operand, &memory) + .unwrap(); + assert_eq!(result, MemoryValue::Address(fp_addr)); + } + + #[test] + fn test_get_value_from_mem_or_fp_or_constant_is_mem_success() { + let mut memory = MemoryManager::default(); + let fp = memory.add(); + let addr_to_read = fp.add_usize::(7).unwrap(); + let expected_val = MemoryValue::::Address(MemoryAddress::new(5, 5)); + memory + .memory + .insert(addr_to_read, expected_val.clone()) + .unwrap(); + + let ctx = RunContext::new(MemoryAddress::new(0, 0), fp); + let operand = MemOrFpOrConstant::MemoryAfterFp { shift: 7 }; + let result = ctx + .get_value_from_mem_or_fp_or_constant(&operand, &memory) + .unwrap(); + assert_eq!(result, expected_val); + } } diff --git a/crates/leanVm/src/core.rs b/crates/leanVm/src/core.rs index 1bf9bacc..52195dfd 100644 --- a/crates/leanVm/src/core.rs +++ b/crates/leanVm/src/core.rs @@ -1,10 +1,16 @@ +#![allow( + clippy::unnecessary_wraps, + clippy::missing_const_for_fn, + clippy::unused_self +)] + use p3_field::PrimeField64; use crate::{ context::run_context::RunContext, errors::{memory::MemoryError, vm::VirtualMachineError}, - memory::manager::MemoryManager, - types::instruction::{Instruction, MemOrFp}, + memory::{address::MemoryAddress, manager::MemoryManager}, + types::instruction::{Instruction, MemOrConstant, MemOrFp, MemOrFpOrConstant, Operation}, }; #[derive(Debug, Default)] @@ -50,7 +56,7 @@ impl VirtualMachine { if is_zero { // **Condition is zero**: The jump is not taken. Advance the `pc` by one. - (*self.run_context.pc() + 1)? + self.run_context.pc().add_usize(1)? } else { // **Condition is non-zero**: Execute the jump. // @@ -64,7 +70,7 @@ impl VirtualMachine { } } else { // For any instruction other than `JumpIfNotZero`, advance the `pc` by one. - (*self.run_context.pc() + 1)? + self.run_context.pc().add_usize(1)? }; // Update the virtual machine's program counter with the calculated next address. @@ -86,7 +92,7 @@ impl VirtualMachine { MemOrFp::Fp => self.run_context.fp, // The instruction specifies updating `fp` to a value from memory. MemOrFp::MemoryAfterFp { shift } => { - let addr = (*self.run_context.fp() + *shift)?; + let addr = self.run_context.fp().add_usize(*shift)?; let value = self .memory_manager .get(addr) @@ -121,6 +127,173 @@ impl VirtualMachine { Ok(()) } + + /// Executes a single instruction, forming one step of the VM's execution cycle. + /// + /// This function is the engine of the virtual machine. It orchestrates the two main phases + /// of a single step: execution and register update. + /// + /// 1. **Execution:** It first matches on the `instruction` variant to dispatch to the appropriate + /// helper method. These helpers are responsible for fetching operands, performing the instruction's core logic, and + /// verifying any required assertions (e.g., that a computed value matches an expected one). + /// + /// 2. **Register Update:** If the execution phase completes successfully, this function then + /// calls `update_registers` to advance the program counter (`pc`) and frame pointer (`fp`) + /// to prepare for the next instruction. + pub fn run_instruction( + &mut self, + instruction: &Instruction, + ) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // Dispatch to the appropriate execution logic based on the instruction type. + match instruction { + // Handle arithmetic operations like ADD and MUL. + Instruction::Computation { + operation, + arg_a, + arg_b, + res, + } => self.execute_computation(operation, arg_a, arg_b, res)?, + + // Handle double-dereference memory operations. + Instruction::Deref { + shift_0, + shift_1, + res, + } => self.execute_deref(*shift_0, *shift_1, res)?, + + // The `JumpIfNotZero` instruction has no execution logic; its effects + // (changing pc and fp) are handled entirely within the register update phase. + Instruction::JumpIfNotZero { .. } => {} + + // Handle the Poseidon2 (16-element) precompile. + Instruction::Poseidon2_16 { shift } => self.execute_poseidon2_16(*shift)?, + + // Handle the Poseidon2 (24-element) precompile. + Instruction::Poseidon2_24 { shift } => self.execute_poseidon2_24(*shift)?, + + // Handle the extension field multiplication precompile. + Instruction::ExtensionMul { args } => self.execute_extension_mul(*args)?, + } + + // After the instruction's core logic has been successfully executed, + // update the pc and fp registers to prepare for the next cycle. + self.update_registers(instruction) + } + + /// Executes a computation instruction (ADD or MUL) and asserts the result. + fn execute_computation( + &self, + operation: &Operation, + arg_a: &MemOrConstant, + arg_b: &MemOrFp, + res: &MemOrConstant, + ) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // Resolve the values of the two input operands. + let val_a = self.run_context.get_value(arg_a, &self.memory_manager)?; + let val_b = self + .run_context + .get_value_from_mem_or_fp(arg_b, &self.memory_manager)?; + + // Perform the arithmetic operation. + let computed = match operation { + Operation::Add => (val_a + val_b)?, + Operation::Mul => (val_a * val_b)?, + }; + + // Resolve the expected result from the `res` operand in memory. + let expected = self.run_context.get_value(res, &self.memory_manager)?; + + // Assert that the computed result matches the value in memory. + if computed != expected { + // If they don't match, return the new, more descriptive error. + return Err(VirtualMachineError::AssertEqFailed { computed, expected }); + } + + Ok(()) + } + + /// Executes a double-dereference instruction (`res = m[m[fp + shift_0] + shift_1]`) and asserts the result. + /// + /// This function handles instructions that require reading a pointer from one memory + /// location to access a value at another. + /// + /// # Errors + /// This function will return an `Err` if: + /// - Any memory access targets an uninitialized memory cell. + /// - The first memory access at `m[fp + shift_0]` does not yield a valid `MemoryAddress`. + /// - The final, dereferenced value does not match the expected value specified by `res`. + fn execute_deref( + &self, + shift_0: usize, + shift_1: usize, + res: &MemOrFpOrConstant, + ) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // Get the address of the cell that holds the pointer. + let pointer_addr = self.run_context.fp().add_usize(shift_0)?; + + // Get the value from that cell, which must be an address (our pointer). + let pointer_val = self + .memory_manager + .get(pointer_addr) + .ok_or(MemoryError::UninitializedMemory(pointer_addr))?; + let pointer = MemoryAddress::try_from(pointer_val)?; + + // Get the final address by adding the second shift to our pointer. + let final_addr = pointer.add_usize(shift_1)?; + + // Get the final value from the dereferenced address. + let final_val = self + .memory_manager + .get(final_addr) + .ok_or(MemoryError::UninitializedMemory(final_addr))?; + + // Resolve the expected result from the `res` operand. + let expected_res = self + .run_context + .get_value_from_mem_or_fp_or_constant(res, &self.memory_manager)?; + + // Assert that the dereferenced value matches the expected result. + if final_val != expected_res { + return Err(VirtualMachineError::AssertEqFailed { + computed: final_val, + expected: expected_res, + }); + } + Ok(()) + } + + fn execute_poseidon2_16(&self, _shift: usize) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // TODO: implement this instruction. + Ok(()) + } + + fn execute_poseidon2_24(&self, _shift: usize) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // TODO: implement this instruction. + Ok(()) + } + + fn execute_extension_mul(&self, _args: [usize; 3]) -> Result<(), VirtualMachineError> + where + F: PrimeField64, + { + // TODO: implement this instruction. + Ok(()) + } } #[cfg(test)] @@ -189,7 +362,11 @@ mod tests { let pc = MemoryAddress::new(0, 10); let fp = MemoryAddress::new(1, 5); // Pre-load memory with a zero value at the address `fp + 1`, which will be our condition. - let mut vm = setup_vm(pc, fp, &[((fp + 1).unwrap(), MemoryValue::Int(F::ZERO))]); + let mut vm = setup_vm( + pc, + fp, + &[(fp.add_usize::(1).unwrap(), MemoryValue::Int(F::ZERO))], + ); // Define a JNZ instruction where the condition points to the zero value. let instruction = Instruction::JumpIfNotZero:: { condition: MemOrConstant::MemoryAfterFp { shift: 1 }, @@ -215,9 +392,15 @@ mod tests { fp, &[ // The condition value (non-zero). - ((fp + 1).unwrap(), MemoryValue::Int(F::from_u64(42))), + ( + fp.add_usize::(1).unwrap(), + MemoryValue::Int(F::from_u64(42)), + ), // The destination address for the jump. - ((fp + 2).unwrap(), MemoryValue::Address(jump_target)), + ( + fp.add_usize::(2).unwrap(), + MemoryValue::Address(jump_target), + ), ], ); // Define a JNZ instruction pointing to the condition and destination. @@ -242,7 +425,7 @@ mod tests { pc, fp, &[( - (fp + 1).unwrap(), + fp.add_usize::(1).unwrap(), MemoryValue::Address(MemoryAddress::new(8, 8)), )], ); @@ -287,7 +470,7 @@ mod tests { let mut vm = setup_vm( MemoryAddress::new(0, 0), fp, - &[((fp + 3).unwrap(), MemoryValue::Address(new_fp))], + &[(fp.add_usize::(3).unwrap(), MemoryValue::Address(new_fp))], ); // Define a JNZ instruction where `updated_fp` points to the new address in memory. let instruction = Instruction::JumpIfNotZero:: { @@ -309,7 +492,10 @@ mod tests { let mut vm = setup_vm( MemoryAddress::new(0, 0), fp, - &[((fp + 3).unwrap(), MemoryValue::Int(F::from_u64(99)))], + &[( + fp.add_usize::(3).unwrap(), + MemoryValue::Int(F::from_u64(99)), + )], ); // Define a JNZ instruction where `updated_fp` points to this integer value. let instruction = Instruction::JumpIfNotZero:: { diff --git a/crates/leanVm/src/errors/math.rs b/crates/leanVm/src/errors/math.rs index 2b44e4b7..baa9e1b3 100644 --- a/crates/leanVm/src/errors/math.rs +++ b/crates/leanVm/src/errors/math.rs @@ -1,9 +1,15 @@ +use p3_field::PrimeField64; use thiserror::Error; use crate::memory::address::MemoryAddress; #[derive(Debug, Error, Eq, PartialEq)] -pub enum MathError { +pub enum MathError +where + F: PrimeField64, +{ #[error("Operation failed: {} + {}, maximum offset value exceeded", 0.0, 0.1)] MemoryAddressAddUsizeOffsetExceeded(Box<(MemoryAddress, usize)>), + #[error("Operation failed: {} + {}, maximum offset value exceeded", 0.0, 0.1)] + MemoryAddressAddFieldOffsetExceeded(Box<(MemoryAddress, F)>), } diff --git a/crates/leanVm/src/errors/memory.rs b/crates/leanVm/src/errors/memory.rs index 588192ac..810da444 100644 --- a/crates/leanVm/src/errors/memory.rs +++ b/crates/leanVm/src/errors/memory.rs @@ -1,5 +1,6 @@ use std::fmt::Debug; +use p3_field::PrimeField64; use thiserror::Error; use super::math::MathError; @@ -8,7 +9,7 @@ use crate::memory::{address::MemoryAddress, val::MemoryValue}; #[derive(Debug, Eq, PartialEq, Error)] pub enum MemoryError where - F: Debug, + F: PrimeField64, { /// Error for when an operation targets a memory segment that has not been allocated. #[error( @@ -35,7 +36,7 @@ where /// Error related to mathematical operations. #[error(transparent)] - Math(#[from] MathError), + Math(#[from] MathError), /// Error when a memory value is expected to be an integer, but it is an address to another memory location. #[error("Memory value should be an integer.")] @@ -47,6 +48,12 @@ where #[error("Memory address is expected but we got an integer.")] ExpectedMemoryAddress, - #[error("Inteer is expected but we got a memory address.")] + #[error("Integer is expected but we got a memory address.")] ExpectedInteger, + + #[error("Operation failed: {} + {}, can't add two address values", 0.0, 0.1)] + MemoryAddressAdd(Box<(MemoryAddress, MemoryAddress)>), + + #[error("Operation failed: {} * {}, can't multiply these two values", 0.0, 0.1)] + InvalidMul(Box<(MemoryValue, MemoryValue)>), } diff --git a/crates/leanVm/src/errors/vm.rs b/crates/leanVm/src/errors/vm.rs index 64f5e804..fee8d717 100644 --- a/crates/leanVm/src/errors/vm.rs +++ b/crates/leanVm/src/errors/vm.rs @@ -1,16 +1,27 @@ use std::fmt::Debug; +use p3_field::PrimeField64; use thiserror::Error; use super::{math::MathError, memory::MemoryError}; +use crate::memory::val::MemoryValue; #[derive(Debug, Error)] pub enum VirtualMachineError where - F: Debug, + F: PrimeField64, { #[error(transparent)] Memory(#[from] MemoryError), #[error(transparent)] - Math(#[from] MathError), + Math(#[from] MathError), + #[error( + "Assertion failed: computed value '{:?}' != expected value '{:?}'.", + computed, + expected + )] + AssertEqFailed { + computed: MemoryValue, + expected: MemoryValue, + }, } diff --git a/crates/leanVm/src/memory/address.rs b/crates/leanVm/src/memory/address.rs index a1d6f18f..0cad456f 100644 --- a/crates/leanVm/src/memory/address.rs +++ b/crates/leanVm/src/memory/address.rs @@ -1,5 +1,6 @@ use std::{fmt::Display, ops::Add}; +use num_traits::cast::ToPrimitive; use p3_field::PrimeField64; #[cfg(test)] use proptest::prelude::*; @@ -14,33 +15,54 @@ pub struct MemoryAddress { } impl MemoryAddress { + #[must_use] pub const fn new(segment_index: usize, offset: usize) -> Self { Self { segment_index, offset, } } -} - -impl Add for MemoryAddress { - type Output = Result; - fn add(self, other: usize) -> Result { + /// Add a `usize` to the address. + pub fn add_usize(self, other: usize) -> Result> { // Try to compute the new offset by adding `other` to the current offset. // // This uses `checked_add` to safely detect any potential `usize` overflow. self.offset .checked_add(other) .map(|offset| Self { - // Keep the same segment index. segment_index: self.segment_index, - // Use the new (safe) offset. offset, }) .ok_or_else(|| MathError::MemoryAddressAddUsizeOffsetExceeded(Box::new((self, other)))) } } +impl Add<&F> for MemoryAddress +where + F: PrimeField64, +{ + type Output = Result>; + + fn add(self, other: &F) -> Self::Output { + // This chained operation safely calculates the new offset. + // - Cast the current `usize` offset to a `u64` to match the field element's type. + // - Add the field element's canonical `u64` value, checking for arithmetic overflow. + // - Chain another operation to convert the `u64` result back into a `usize`. + // - If any of the chained steps returned `None`, create the specific overflow error. + let new_offset = ((self.offset as u64).checked_add(other.as_canonical_u64())) + .and_then(|x| x.to_usize()) + .ok_or_else(|| { + MathError::MemoryAddressAddFieldOffsetExceeded(Box::new((self, *other))) + })?; + + // If the addition was successful, create a new address with + // - the same segment, + // - the newly calculated offset. + Ok(Self::new(self.segment_index, new_offset)) + } +} + impl Display for MemoryAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}", self.segment_index, self.offset) @@ -97,7 +119,7 @@ mod tests { segment_index: 2, offset: 100, }; - let result = addr + 25; + let result = addr.add_usize::(25); assert_eq!( result, Ok(MemoryAddress { @@ -113,7 +135,7 @@ mod tests { segment_index: 5, offset: 500, }; - let result = addr + 0; + let result = addr.add_usize::(0); assert_eq!(result, Ok(addr)); } @@ -123,7 +145,7 @@ mod tests { segment_index: 1, offset: usize::MAX, }; - let result = addr + 1; + let result = addr.add_usize::(1); match result { Err(MathError::MemoryAddressAddUsizeOffsetExceeded(boxed)) => { let (original, added) = *boxed; @@ -138,7 +160,7 @@ mod tests { proptest! { #[test] fn test_add_does_not_overflow(addr in any::(), delta in 0usize..1_000_000) { - let result = addr + delta; + let result = addr.add_usize::(delta); // Only test when offset + delta won't overflow if let Some(expected_offset) = addr.offset.checked_add(delta) { prop_assert_eq!(result, Ok(MemoryAddress { @@ -192,4 +214,47 @@ mod tests { // Assert the specific error is ExpectedMemoryAddress assert_eq!(result.unwrap_err(), MemoryError::ExpectedMemoryAddress); } + + #[test] + fn test_add_field_element_success() { + // Setup: An initial address and a field element to add. + let addr = MemoryAddress::new(2, 100); + let val = F::from_u64(50); + + // Execute: Add the field element to the address using the `+` operator. + let result = addr + &val; + + // Verify: The result should be `Ok` and the offset should be updated correctly, + // while the segment index remains the same. + assert_eq!(result, Ok(MemoryAddress::new(2, 150))); + } + + #[test] + fn test_add_field_element_zero() { + // Setup: An initial address. + let addr = MemoryAddress::new(3, 123); + let val = F::ZERO; + + // Execute: Add zero to the address. + let result = addr + &val; + + // Verify: The address should remain unchanged. + assert_eq!(result, Ok(addr)); + } + + #[test] + fn test_add_field_element_overflow() { + // Setup: An address with the maximum possible offset. + let addr = MemoryAddress::new(1, usize::MAX); + let val = F::ONE; + + // Execute: Add 1, which should cause an overflow. + let result = addr + &val; + + // Verify: The result should be an `Err` with the specific offset overflow variant. + assert!(matches!( + result, + Err(MathError::MemoryAddressAddFieldOffsetExceeded(_)) + )); + } } diff --git a/crates/leanVm/src/memory/manager.rs b/crates/leanVm/src/memory/manager.rs index 41ca2926..7462dad9 100644 --- a/crates/leanVm/src/memory/manager.rs +++ b/crates/leanVm/src/memory/manager.rs @@ -81,7 +81,7 @@ impl MemoryManager { // Compute the target address: ptr + num. // // This operation may fail if it causes overflow. - let addr = (ptr + num).map_err(MemoryError::Math)?; + let addr = ptr.add_usize(num).map_err(MemoryError::Math)?; // Attempt to write the value into memory at the computed address. // @@ -93,7 +93,7 @@ impl MemoryManager { // After writing all values, compute and return the address after the last item. // // This is simply ptr + data.len(), and it may also fail on overflow. - (ptr + data.len()).map_err(MemoryError::Math) + ptr.add_usize(data.len()).map_err(MemoryError::Math) } /// Retrieves the value stored at a given memory address. diff --git a/crates/leanVm/src/memory/val.rs b/crates/leanVm/src/memory/val.rs index 9679125a..58c23b5e 100644 --- a/crates/leanVm/src/memory/val.rs +++ b/crates/leanVm/src/memory/val.rs @@ -1,3 +1,5 @@ +use std::ops::{Add, Mul}; + use p3_field::PrimeField64; #[cfg(test)] use proptest::prelude::*; @@ -23,6 +25,54 @@ where } } +impl Add for MemoryValue +where + F: PrimeField64, +{ + type Output = Result>; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + // Case 1: Add two integers, returning a new integer. + (Self::Int(a), Self::Int(b)) => Ok(Self::Int(a + b)), + + // Case 2 & 3: Add an integer to an address (pointer arithmetic). + (Self::Address(address), Self::Int(int)) | (Self::Int(int), Self::Address(address)) => { + // The address computation by adding the integer to the address is safe. + let new_address = (address + &int)?; + Ok(Self::Address(new_address)) + } + + // Case 4: Adding two addresses is an invalid operation. + (Self::Address(a), Self::Address(b)) => { + Err(MemoryError::MemoryAddressAdd(Box::new((a, b)))) + } + } + } +} + +impl Mul for MemoryValue +where + F: PrimeField64, +{ + type Output = Result>; + + fn mul(self, rhs: Self) -> Self::Output { + match (self.clone(), rhs.clone()) { + // Case 1 (Success): Multiply two integers. + (Self::Int(a), Self::Int(b)) => Ok(Self::Int(a * b)), + + // Case 2 (Error): Any other combination is invalid for multiplication. + // + // This includes: + // - Address * Int, + // - Int * Address, + // - Address * Address. + _ => Err(MemoryError::InvalidMul(Box::new((self, rhs)))), + } + } +} + #[cfg(test)] impl Arbitrary for MemoryValue where @@ -48,6 +98,7 @@ mod tests { use p3_field::PrimeCharacteristicRing; use super::*; + use crate::errors::math::MathError; type F = BabyBear; @@ -89,4 +140,117 @@ mod tests { // Assert the specific error is ExpectedInteger assert_eq!(result.unwrap_err(), MemoryError::ExpectedInteger); } + + #[test] + fn test_add_two_ints_success() { + // Setup: Create two integer memory values. + let val1 = MemoryValue::Int(F::from_u64(100)); + let val2 = MemoryValue::Int(F::from_u64(50)); + + // Action: Add the two values together. + let result = (val1 + val2).unwrap(); + + // Verify: The result should be an integer with the correct sum. + assert_eq!(result, MemoryValue::Int(F::from_u64(150))); + } + + #[test] + fn test_add_int_to_address_success() { + // Setup: Create an address and an integer value. + let addr = MemoryAddress::new(1, 10); + let val1 = MemoryValue::Address(addr); + let val2 = MemoryValue::Int(F::from_u64(5)); + + // Action: Add the integer to the address. + let result = (val1 + val2).unwrap(); + + // Verify: The result should be a new address with the offset correctly incremented. + assert_eq!(result, MemoryValue::Address(MemoryAddress::new(1, 15))); + } + + #[test] + fn test_add_address_to_int_success() { + // Setup: Create an integer and an address value (commutative test). + let addr = MemoryAddress::new(1, 10); + let val1 = MemoryValue::Int(F::from_u64(5)); + let val2 = MemoryValue::Address(addr); + + // Action: Add the address to the integer. + let result = (val1 + val2).unwrap(); + + // Verify: The result should be a new address with the offset correctly incremented. + assert_eq!(result, MemoryValue::Address(MemoryAddress::new(1, 15))); + } + + #[test] + fn test_add_int_to_address_overflow_fails() { + // Setup: Create an address at the maximum possible offset and an integer value of 1. + let addr = MemoryAddress::new(1, usize::MAX); + let val1 = MemoryValue::Address(addr); + let val2 = MemoryValue::Int(F::from_u64(1)); + + // Action: Attempt to add the integer, which should cause an overflow. + let err = (val1 + val2).unwrap_err(); + + // Verify: The operation should fail with the specific offset overflow error. + assert!(matches!( + err, + MemoryError::Math(MathError::MemoryAddressAddFieldOffsetExceeded(_)) + )); + } + + #[test] + fn test_add_two_addresses_fails() { + // Setup: Create two address values. + let addr1 = MemoryAddress::new(1, 10); + let addr2 = MemoryAddress::new(2, 20); + let val1 = MemoryValue::::Address(addr1); + let val2 = MemoryValue::::Address(addr2); + + // Action: Attempt to add the two addresses. + let err = (val1 + val2).unwrap_err(); + + // Verify: The operation should fail, as adding two addresses is not a valid operation. + assert!(matches!(err, MemoryError::MemoryAddressAdd(_))); + } + + #[test] + fn test_mul_two_ints_success() { + // Setup: Create two integer memory values. + let val1 = MemoryValue::Int(F::from_u64(10)); + let val2 = MemoryValue::Int(F::from_u64(5)); + + // Action: Multiply the two values. + let result = (val1 * val2).unwrap(); + + // Verify: The result should be an integer with the correct product. + assert_eq!(result, MemoryValue::Int(F::from_u64(50))); + } + + #[test] + fn test_mul_int_and_address_fails() { + // Setup: Create an address and an integer value. + let val1 = MemoryValue::Address(MemoryAddress::new(1, 10)); + let val2 = MemoryValue::Int(F::from_u64(5)); + + // Action: Attempt to multiply them. + let err = (val1.clone() * val2.clone()).unwrap_err(); + + // Verify: The operation should fail with the specific `InvalidMul` error, + // containing the values that caused the failure. + assert!(matches!(err, MemoryError::InvalidMul(boxed) if *boxed == (val1, val2))); + } + + #[test] + fn test_mul_two_addresses_fails() { + // Setup: Create two address values. + let val1 = MemoryValue::::Address(MemoryAddress::new(1, 10)); + let val2 = MemoryValue::::Address(MemoryAddress::new(2, 20)); + + // Action: Attempt to multiply them. + let err = (val1.clone() * val2.clone()).unwrap_err(); + + // Verify: The operation should fail with the `InvalidMul` error. + assert!(matches!(err, MemoryError::InvalidMul(boxed) if *boxed == (val1, val2))); + } } From 0af47a66e18cd01a0fe102c89fa3265ed1d062bf Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Mon, 28 Jul 2025 19:07:54 +0200 Subject: [PATCH 2/2] rm core for now --- crates/leanVm/src/core.rs | 177 +------------------------------------- 1 file changed, 2 insertions(+), 175 deletions(-) diff --git a/crates/leanVm/src/core.rs b/crates/leanVm/src/core.rs index 52195dfd..2812b01d 100644 --- a/crates/leanVm/src/core.rs +++ b/crates/leanVm/src/core.rs @@ -1,16 +1,10 @@ -#![allow( - clippy::unnecessary_wraps, - clippy::missing_const_for_fn, - clippy::unused_self -)] - use p3_field::PrimeField64; use crate::{ context::run_context::RunContext, errors::{memory::MemoryError, vm::VirtualMachineError}, - memory::{address::MemoryAddress, manager::MemoryManager}, - types::instruction::{Instruction, MemOrConstant, MemOrFp, MemOrFpOrConstant, Operation}, + memory::manager::MemoryManager, + types::instruction::{Instruction, MemOrFp}, }; #[derive(Debug, Default)] @@ -127,173 +121,6 @@ impl VirtualMachine { Ok(()) } - - /// Executes a single instruction, forming one step of the VM's execution cycle. - /// - /// This function is the engine of the virtual machine. It orchestrates the two main phases - /// of a single step: execution and register update. - /// - /// 1. **Execution:** It first matches on the `instruction` variant to dispatch to the appropriate - /// helper method. These helpers are responsible for fetching operands, performing the instruction's core logic, and - /// verifying any required assertions (e.g., that a computed value matches an expected one). - /// - /// 2. **Register Update:** If the execution phase completes successfully, this function then - /// calls `update_registers` to advance the program counter (`pc`) and frame pointer (`fp`) - /// to prepare for the next instruction. - pub fn run_instruction( - &mut self, - instruction: &Instruction, - ) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // Dispatch to the appropriate execution logic based on the instruction type. - match instruction { - // Handle arithmetic operations like ADD and MUL. - Instruction::Computation { - operation, - arg_a, - arg_b, - res, - } => self.execute_computation(operation, arg_a, arg_b, res)?, - - // Handle double-dereference memory operations. - Instruction::Deref { - shift_0, - shift_1, - res, - } => self.execute_deref(*shift_0, *shift_1, res)?, - - // The `JumpIfNotZero` instruction has no execution logic; its effects - // (changing pc and fp) are handled entirely within the register update phase. - Instruction::JumpIfNotZero { .. } => {} - - // Handle the Poseidon2 (16-element) precompile. - Instruction::Poseidon2_16 { shift } => self.execute_poseidon2_16(*shift)?, - - // Handle the Poseidon2 (24-element) precompile. - Instruction::Poseidon2_24 { shift } => self.execute_poseidon2_24(*shift)?, - - // Handle the extension field multiplication precompile. - Instruction::ExtensionMul { args } => self.execute_extension_mul(*args)?, - } - - // After the instruction's core logic has been successfully executed, - // update the pc and fp registers to prepare for the next cycle. - self.update_registers(instruction) - } - - /// Executes a computation instruction (ADD or MUL) and asserts the result. - fn execute_computation( - &self, - operation: &Operation, - arg_a: &MemOrConstant, - arg_b: &MemOrFp, - res: &MemOrConstant, - ) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // Resolve the values of the two input operands. - let val_a = self.run_context.get_value(arg_a, &self.memory_manager)?; - let val_b = self - .run_context - .get_value_from_mem_or_fp(arg_b, &self.memory_manager)?; - - // Perform the arithmetic operation. - let computed = match operation { - Operation::Add => (val_a + val_b)?, - Operation::Mul => (val_a * val_b)?, - }; - - // Resolve the expected result from the `res` operand in memory. - let expected = self.run_context.get_value(res, &self.memory_manager)?; - - // Assert that the computed result matches the value in memory. - if computed != expected { - // If they don't match, return the new, more descriptive error. - return Err(VirtualMachineError::AssertEqFailed { computed, expected }); - } - - Ok(()) - } - - /// Executes a double-dereference instruction (`res = m[m[fp + shift_0] + shift_1]`) and asserts the result. - /// - /// This function handles instructions that require reading a pointer from one memory - /// location to access a value at another. - /// - /// # Errors - /// This function will return an `Err` if: - /// - Any memory access targets an uninitialized memory cell. - /// - The first memory access at `m[fp + shift_0]` does not yield a valid `MemoryAddress`. - /// - The final, dereferenced value does not match the expected value specified by `res`. - fn execute_deref( - &self, - shift_0: usize, - shift_1: usize, - res: &MemOrFpOrConstant, - ) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // Get the address of the cell that holds the pointer. - let pointer_addr = self.run_context.fp().add_usize(shift_0)?; - - // Get the value from that cell, which must be an address (our pointer). - let pointer_val = self - .memory_manager - .get(pointer_addr) - .ok_or(MemoryError::UninitializedMemory(pointer_addr))?; - let pointer = MemoryAddress::try_from(pointer_val)?; - - // Get the final address by adding the second shift to our pointer. - let final_addr = pointer.add_usize(shift_1)?; - - // Get the final value from the dereferenced address. - let final_val = self - .memory_manager - .get(final_addr) - .ok_or(MemoryError::UninitializedMemory(final_addr))?; - - // Resolve the expected result from the `res` operand. - let expected_res = self - .run_context - .get_value_from_mem_or_fp_or_constant(res, &self.memory_manager)?; - - // Assert that the dereferenced value matches the expected result. - if final_val != expected_res { - return Err(VirtualMachineError::AssertEqFailed { - computed: final_val, - expected: expected_res, - }); - } - Ok(()) - } - - fn execute_poseidon2_16(&self, _shift: usize) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // TODO: implement this instruction. - Ok(()) - } - - fn execute_poseidon2_24(&self, _shift: usize) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // TODO: implement this instruction. - Ok(()) - } - - fn execute_extension_mul(&self, _args: [usize; 3]) -> Result<(), VirtualMachineError> - where - F: PrimeField64, - { - // TODO: implement this instruction. - Ok(()) - } } #[cfg(test)]