Skip to content

Commit 1867e59

Browse files
authored
memory: add memory manager (#4)
* memory: add memory manager * fmt * fmt * better doc * make clippy happy
1 parent 79610d3 commit 1867e59

File tree

10 files changed

+364
-2
lines changed

10 files changed

+364
-2
lines changed

crates/leanVm/src/context/run_context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::memory::address::MemoryAddress;
22

3-
#[derive(Debug)]
3+
#[derive(Debug, Default)]
44
pub struct RunContext {
55
/// The address in memory of the current instruction to be executed.
66
pub(crate) pc: MemoryAddress,

crates/leanVm/src/core.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use crate::{context::run_context::RunContext, memory::manager::MemoryManager};
2+
3+
#[derive(Debug, Default)]
4+
pub struct VirtualMachine {
5+
pub(crate) run_context: RunContext,
6+
pub memory_manager: MemoryManager,
7+
}

crates/leanVm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod context;
2+
pub mod core;
23
pub mod memory;
34
pub mod types;

crates/leanVm/src/memory/address.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
1+
use std::{fmt::Display, ops::Add};
2+
13
#[cfg(test)]
24
use proptest::prelude::*;
35

6+
use crate::types::math_errors::MathError;
7+
48
#[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Copy, Debug, Default)]
59
pub struct MemoryAddress {
610
pub segment_index: usize,
711
pub offset: usize,
812
}
913

14+
impl Add<usize> for MemoryAddress {
15+
type Output = Result<Self, MathError>;
16+
17+
fn add(self, other: usize) -> Result<Self, MathError> {
18+
// Try to compute the new offset by adding `other` to the current offset.
19+
//
20+
// This uses `checked_add` to safely detect any potential `usize` overflow.
21+
self.offset
22+
.checked_add(other)
23+
.map(|offset| Self {
24+
// Keep the same segment index.
25+
segment_index: self.segment_index,
26+
// Use the new (safe) offset.
27+
offset,
28+
})
29+
.ok_or_else(|| MathError::MemoryAddressAddUsizeOffsetExceeded(Box::new((self, other))))
30+
}
31+
}
32+
33+
impl Display for MemoryAddress {
34+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35+
write!(f, "{}:{}", self.segment_index, self.offset)
36+
}
37+
}
38+
1039
#[cfg(test)]
1140
impl Arbitrary for MemoryAddress {
1241
type Parameters = ();
@@ -26,3 +55,73 @@ impl Arbitrary for MemoryAddress {
2655
.boxed()
2756
}
2857
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use proptest::prelude::*;
62+
63+
use super::*;
64+
65+
#[test]
66+
fn test_add_usize_success() {
67+
let addr = MemoryAddress {
68+
segment_index: 2,
69+
offset: 100,
70+
};
71+
let result = addr + 25;
72+
assert_eq!(
73+
result,
74+
Ok(MemoryAddress {
75+
segment_index: 2,
76+
offset: 125
77+
})
78+
);
79+
}
80+
81+
#[test]
82+
fn test_add_zero_offset() {
83+
let addr = MemoryAddress {
84+
segment_index: 5,
85+
offset: 500,
86+
};
87+
let result = addr + 0;
88+
assert_eq!(result, Ok(addr));
89+
}
90+
91+
#[test]
92+
fn test_add_usize_overflow() {
93+
let addr = MemoryAddress {
94+
segment_index: 1,
95+
offset: usize::MAX,
96+
};
97+
let result = addr + 1;
98+
match result {
99+
Err(MathError::MemoryAddressAddUsizeOffsetExceeded(boxed)) => {
100+
let (original, added) = *boxed;
101+
assert_eq!(original.segment_index, 1);
102+
assert_eq!(original.offset, usize::MAX);
103+
assert_eq!(added, 1);
104+
}
105+
_ => panic!("Expected overflow error, got: {result:?}"),
106+
}
107+
}
108+
109+
proptest! {
110+
#[test]
111+
fn test_add_does_not_overflow(addr in any::<MemoryAddress>(), delta in 0usize..1_000_000) {
112+
let result = addr + delta;
113+
// Only test when offset + delta won't overflow
114+
if let Some(expected_offset) = addr.offset.checked_add(delta) {
115+
prop_assert_eq!(result, Ok(MemoryAddress {
116+
segment_index: addr.segment_index,
117+
offset: expected_offset,
118+
}));
119+
} else {
120+
prop_assert!(matches!(
121+
result,
122+
Err(MathError::MemoryAddressAddUsizeOffsetExceeded(_))
123+
));
124+
}
125+
}
126+
}
127+
}

crates/leanVm/src/memory/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::fmt::Debug;
33
use thiserror::Error;
44

55
use super::{address::MemoryAddress, val::MemoryValue};
6+
use crate::types::math_errors::MathError;
67

78
#[derive(Debug, Eq, PartialEq, Error)]
89
pub enum MemoryError<F>
@@ -31,4 +32,8 @@ where
3132
"Memory overflow: the requested memory address is too large and exceeds the machine's capacity."
3233
)]
3334
VecCapacityExceeded,
35+
36+
/// Error related to mathematical operations.
37+
#[error(transparent)]
38+
Math(#[from] MathError),
3439
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use p3_field::PrimeField64;
2+
3+
use super::{address::MemoryAddress, error::MemoryError, mem::Memory, val::MemoryValue};
4+
5+
/// A high level manager for the memory.
6+
#[derive(Debug, Default)]
7+
pub struct MemoryManager {
8+
pub memory: Memory,
9+
}
10+
11+
impl MemoryManager {
12+
/// Returns the number of currently allocated segments in memory.
13+
///
14+
/// This reflects the actual physical segment count, starting from segment index 0.
15+
///
16+
/// # Returns
17+
/// * `usize` — the number of segments allocated in `self.memory`.
18+
#[must_use]
19+
pub fn num_segments(&self) -> usize {
20+
self.memory.data.len()
21+
}
22+
23+
/// Adds a new, empty segment to memory and returns its starting address.
24+
///
25+
/// This operation appends an empty segment to the memory, which starts at offset 0.
26+
/// The returned `MemoryAddress` corresponds to the beginning of that segment.
27+
///
28+
/// # Returns
29+
/// * `MemoryAddress` — the starting address (segment_index, offset=0) of the new segment.
30+
pub fn add(&mut self) -> MemoryAddress {
31+
// Allocate a new, empty segment at the end of the list.
32+
self.memory.data.push(Vec::new());
33+
34+
// Compute the index of the newly created segment.
35+
let new_segment_index = self.memory.data.len() - 1;
36+
37+
// Return the starting address of the new segment (offset always 0).
38+
MemoryAddress {
39+
segment_index: new_segment_index,
40+
offset: 0,
41+
}
42+
}
43+
44+
/// Loads a slice of data into memory starting from a given address.
45+
///
46+
/// The function writes each value in `data` to consecutive addresses starting
47+
/// from `ptr`. The write is done in reverse order to ensure that any required
48+
/// memory extension happens once at the end rather than multiple times.
49+
///
50+
/// If all values are written successfully, the function returns the first
51+
/// address **after** the last inserted value.
52+
///
53+
/// # Type Parameters
54+
/// * `F`: A finite field, used as the scalar type.
55+
///
56+
/// # Arguments
57+
/// * `ptr`: Starting address where the data should be written.
58+
/// * `data`: A slice of memory values representing the values to be stored.
59+
///
60+
/// # Returns
61+
/// * `Ok(MemoryAddress)` — the address immediately following the last written value.
62+
/// * `Err(MemoryError<F>)` — if writing fails due to:
63+
/// - Memory cell already initialized with a different value.
64+
/// - Overflow when computing addresses.
65+
/// - Exceeding vector capacity.
66+
pub fn load_data<F>(
67+
&mut self,
68+
ptr: MemoryAddress,
69+
data: &[MemoryValue<F>],
70+
) -> Result<MemoryAddress, MemoryError<F>>
71+
where
72+
F: PrimeField64,
73+
{
74+
// Iterate over the data values in reverse order, with indices.
75+
//
76+
// This reverse order allows any required memory segment resizing
77+
// (e.g., length extension or capacity reservation) to occur *once*
78+
// at the highest offset instead of repeatedly during writes.
79+
for (num, value) in data.iter().enumerate().rev() {
80+
// Compute the target address: ptr + num.
81+
//
82+
// This operation may fail if it causes overflow.
83+
let addr = (ptr + num).map_err(MemoryError::Math)?;
84+
85+
// Attempt to write the value into memory at the computed address.
86+
//
87+
// This enforces the write-once rule — it will fail if the cell is already
88+
// initialized with a different value.
89+
self.memory.insert(addr, value.clone())?;
90+
}
91+
92+
// After writing all values, compute and return the address after the last item.
93+
//
94+
// This is simply ptr + data.len(), and it may also fail on overflow.
95+
(ptr + data.len()).map_err(MemoryError::Math)
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
use p3_baby_bear::BabyBear;
102+
use p3_field::PrimeCharacteristicRing;
103+
104+
use super::*;
105+
use crate::{memory::cell::MemoryCell, types::math_errors::MathError};
106+
107+
type F = BabyBear;
108+
109+
#[test]
110+
fn test_add_segment_returns_correct_address() {
111+
// Create a new empty memory manager.
112+
let mut manager = MemoryManager::default();
113+
114+
// Initially, there should be no segments.
115+
assert_eq!(manager.num_segments(), 0);
116+
117+
// Add the first memory segment.
118+
let addr1 = manager.add();
119+
120+
// The first segment should have index 0, starting at offset 0.
121+
assert_eq!(addr1.segment_index, 0);
122+
assert_eq!(addr1.offset, 0);
123+
124+
// After adding, the total number of segments should be 1.
125+
assert_eq!(manager.num_segments(), 1);
126+
127+
// Add another segment.
128+
let addr2 = manager.add();
129+
130+
// The second segment should have index 1, also starting at offset 0.
131+
assert_eq!(addr2.segment_index, 1);
132+
assert_eq!(addr2.offset, 0);
133+
134+
// Now there should be exactly 2 segments.
135+
assert_eq!(manager.num_segments(), 2);
136+
}
137+
138+
#[test]
139+
fn test_load_data_successful() {
140+
// Create a new memory manager.
141+
let mut manager = MemoryManager::default();
142+
143+
// Add a memory segment and get its starting address.
144+
let base_addr = manager.add(); // segment_index = 0, offset = 0
145+
146+
// Prepare a list of memory values to load.
147+
let values = vec![
148+
MemoryValue::Int(F::from_u64(10)),
149+
MemoryValue::Int(F::from_u64(20)),
150+
MemoryValue::Int(F::from_u64(30)),
151+
];
152+
153+
// Load the data into memory starting at base_addr.
154+
let end_addr = manager.load_data(base_addr, &values).unwrap();
155+
156+
// The returned end address should be immediately after the last inserted value.
157+
assert_eq!(end_addr.segment_index, base_addr.segment_index);
158+
assert_eq!(end_addr.offset, base_addr.offset + values.len());
159+
160+
// Verify that each value was inserted correctly at its expected offset.
161+
for (i, expected) in values.iter().enumerate() {
162+
let addr = MemoryAddress {
163+
segment_index: base_addr.segment_index,
164+
offset: base_addr.offset + i,
165+
};
166+
assert_eq!(manager.memory.get(addr), Some(expected.clone()));
167+
}
168+
}
169+
170+
#[test]
171+
fn test_load_data_returns_math_error_on_address_overflow() {
172+
// Create a new memory manager.
173+
let mut manager = MemoryManager::default();
174+
175+
// Define a starting address where the offset is at the maximum `usize` value.
176+
let base_addr = MemoryAddress {
177+
segment_index: 0,
178+
offset: usize::MAX,
179+
};
180+
181+
// Manually push an empty segment to allow writing into segment 0.
182+
manager.memory.data.push(Vec::new());
183+
184+
// Create two values to write: this will cause an overflow.
185+
let values = vec![
186+
MemoryValue::Int(F::from_u64(1)),
187+
MemoryValue::Int(F::from_u64(2)),
188+
];
189+
190+
// Try to load data starting at MAX offset.
191+
//
192+
// This should fail with an overflow error.
193+
let err = manager.load_data(base_addr, &values).unwrap_err();
194+
195+
// Confirm that the error is a MathError due to offset overflow.
196+
match err {
197+
MemoryError::Math(MathError::MemoryAddressAddUsizeOffsetExceeded(boxed)) => {
198+
let (addr, delta) = *boxed;
199+
// original address
200+
assert_eq!(addr, base_addr);
201+
// the amount that caused overflow
202+
assert_eq!(delta, 1);
203+
}
204+
other => panic!("Unexpected error: {other:?}"),
205+
}
206+
}
207+
208+
#[test]
209+
fn test_load_data_partial_write_does_not_corrupt_memory() {
210+
// Create a new memory manager.
211+
let mut manager = MemoryManager::default();
212+
213+
// Add a segment (segment_index = 0).
214+
let _ = manager.add();
215+
216+
// Construct values to insert. The second will cause a failure.
217+
let values = vec![
218+
MemoryValue::Int(F::from_u64(1)),
219+
MemoryValue::Int(F::from_u64(2)),
220+
];
221+
222+
// Set the starting address such that adding even one to it will cause overflow.
223+
let failing_addr = MemoryAddress {
224+
segment_index: 0,
225+
offset: usize::MAX - 1,
226+
};
227+
228+
// Simulate a memory segment with a small preallocated length.
229+
manager.memory.data[0] = vec![MemoryCell::NONE; 4];
230+
231+
// Attempt to load the data starting at the failing address.
232+
// This should fail due to memory limitations.
233+
let err = manager.load_data(failing_addr, &values).unwrap_err();
234+
235+
// Confirm the error is due to exceeding vector capacity.
236+
assert!(matches!(err, MemoryError::VecCapacityExceeded));
237+
}
238+
}

crates/leanVm/src/memory/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod address;
22
pub mod cell;
33
pub mod error;
4+
pub mod manager;
45
pub mod mem;
56
pub mod val;

0 commit comments

Comments
 (0)