Skip to content

Commit 17774fb

Browse files
authored
memory: design skeleton with simplest operations (#2)
1 parent ceb7c5a commit 17774fb

File tree

8 files changed

+609
-49
lines changed

8 files changed

+609
-49
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ lean-vm = { path = "crates/leanVm" }
3939
p3-field = { git = "https://github.com/Plonky3/Plonky3.git", rev = "d0c4a36" }
4040
p3-baby-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "d0c4a36" }
4141
p3-koala-bear = { git = "https://github.com/Plonky3/Plonky3.git", rev = "d0c4a36" }
42+
43+
thiserror = "2.0"
44+
proptest = "1.7"

crates/leanVm/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ workspace = true
1212
p3-baby-bear.workspace = true
1313
p3-koala-bear.workspace = true
1414
p3-field.workspace = true
15+
16+
thiserror.workspace = true
17+
18+
[dev-dependencies]
19+
proptest.workspace = true
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#[cfg(test)]
2+
use proptest::prelude::*;
3+
4+
#[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone, Copy, Debug, Default)]
5+
pub struct MemoryAddress {
6+
pub segment_index: usize,
7+
pub offset: usize,
8+
}
9+
10+
#[cfg(test)]
11+
impl Arbitrary for MemoryAddress {
12+
type Parameters = ();
13+
type Strategy = BoxedStrategy<Self>;
14+
15+
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
16+
(
17+
// segment_index fits in 29 bits
18+
0..((1u64 << 29) - 1) as usize,
19+
// offset fits in 32 bits
20+
0..((1u64 << 32) - 1) as usize,
21+
)
22+
.prop_map(|(segment_index, offset)| Self {
23+
segment_index,
24+
offset,
25+
})
26+
.boxed()
27+
}
28+
}

crates/leanVm/src/memory/cell.rs

Lines changed: 194 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::ops::{Deref, DerefMut};
22

33
use p3_field::PrimeField64;
44

5+
use super::{address::MemoryAddress, val::MemoryValue};
6+
57
/// A memory cell used by the VM for storing 64-bit field elements with metadata.
68
///
79
/// Internally, the cell holds a single `u64` value.
@@ -34,26 +36,13 @@ impl MemoryCell {
3436
pub(crate) const NONE_MASK: u64 = 1 << 63;
3537
/// Flag bit indicating the cell was accessed (bit 62 set).
3638
pub(crate) const ACCESS_MASK: u64 = 1 << 62;
39+
/// Flag bit indicating the cell contains a MemoryAddress (bit 61 set).
40+
pub(crate) const ADDRESS_MASK: u64 = 1 << 61;
41+
/// A mask to extract only the value bits, ignoring all flags.
42+
pub(crate) const VALUE_MASK: u64 = 0x1FFF_FFFF_FFFF_FFFF;
3743
/// Constant representing an empty cell.
3844
pub(crate) const NONE: Self = Self(Self::NONE_MASK);
3945

40-
/// Creates a `MemoryCell` from a field element, using its canonical `u64` representation.
41-
///
42-
/// This clears any flag bits and assumes the value is valid.
43-
pub(crate) fn from_f<F>(value: F) -> Self
44-
where
45-
F: PrimeField64,
46-
{
47-
Self(value.as_canonical_u64())
48-
}
49-
50-
/// Creates a raw `MemoryCell` from a `u64` value.
51-
///
52-
/// Caller is responsible for ensuring no flag bits are set unless intentional.
53-
pub(crate) const fn from_u64(value: u64) -> Self {
54-
Self(value)
55-
}
56-
5746
/// Returns true if the cell is marked as empty (`NONE`).
5847
pub(crate) const fn is_none(self) -> bool {
5948
self.0 & Self::NONE_MASK == Self::NONE_MASK
@@ -73,67 +62,223 @@ impl MemoryCell {
7362
pub(crate) const fn is_accessed(self) -> bool {
7463
self.0 & Self::ACCESS_MASK == Self::ACCESS_MASK
7564
}
65+
66+
pub(crate) fn value<F>(self) -> Option<MemoryValue<F>>
67+
where
68+
MemoryValue<F>: From<Self>,
69+
{
70+
self.is_some().then(|| self.into())
71+
}
72+
}
73+
74+
impl<F> From<MemoryValue<F>> for MemoryCell
75+
where
76+
F: PrimeField64,
77+
{
78+
fn from(value: MemoryValue<F>) -> Self {
79+
match value {
80+
// If it's an integer, store its u64 representation.
81+
// The ADDRESS_MASK bit will be 0 by default.
82+
MemoryValue::Int(f) => Self(f.as_canonical_u64()),
83+
84+
// If it's an address, pack it into the u64.
85+
MemoryValue::Address(addr) => {
86+
// Ensure the address components fit within their allocated bit-space.
87+
// 29 bits for segment allows for 536+ million segments.
88+
// 32 bits for offset allows for 4+ billion items per segment.
89+
debug_assert!(
90+
addr.segment_index < (1 << 29),
91+
"Segment index out of bounds"
92+
);
93+
debug_assert!(addr.offset < (1 << 32), "Offset out of bounds");
94+
95+
// Pack segment and offset into a single u64, and set the address flag.
96+
let segment = (addr.segment_index as u64) << 32;
97+
let offset = addr.offset as u64;
98+
Self(segment | offset | Self::ADDRESS_MASK)
99+
}
100+
}
101+
}
102+
}
103+
104+
impl<F> From<MemoryCell> for MemoryValue<F>
105+
where
106+
F: PrimeField64,
107+
{
108+
fn from(cell: MemoryCell) -> Self {
109+
// Check the address flag to determine the type of value.
110+
if (cell.0 & MemoryCell::ADDRESS_MASK) == MemoryCell::ADDRESS_MASK {
111+
// It's an address, so we unpack it.
112+
let segment_index = ((cell.0 & MemoryCell::VALUE_MASK) >> 32) as usize;
113+
// Mask for lower 32 bits
114+
let offset = (cell.0 & 0xFFFF_FFFF) as usize;
115+
116+
Self::Address(MemoryAddress {
117+
segment_index,
118+
offset,
119+
})
120+
} else {
121+
// It's an integer. We extract the value bits and convert to a field element.
122+
let value_bits = cell.0 & MemoryCell::VALUE_MASK;
123+
Self::Int(F::from_u64(value_bits))
124+
}
125+
}
76126
}
77127

78128
#[cfg(test)]
79129
mod tests {
80130
use p3_baby_bear::BabyBear;
81131
use p3_field::PrimeCharacteristicRing;
132+
use proptest::prelude::*;
82133

83134
use super::*;
84135

85136
type F = BabyBear;
86137

87-
#[test]
88-
fn test_from_f_and_accessors() {
89-
let f = F::from_u64(123);
90-
let cell = MemoryCell::from_f(f);
91-
assert_eq!(*cell, 123);
92-
assert!(cell.is_some());
93-
assert!(!cell.is_none());
94-
assert!(!cell.is_accessed());
95-
}
96-
97-
#[test]
98-
fn test_from_u64_and_flags() {
99-
let raw = 0xFFFF_FFFF;
100-
let cell = MemoryCell::from_u64(raw);
101-
assert_eq!(*cell, raw);
102-
assert!(cell.is_some());
103-
assert!(!cell.is_none());
104-
}
105-
106138
#[test]
107139
fn test_is_none_and_is_some() {
140+
// A cell explicitly created as NONE should be none.
108141
let none_cell = MemoryCell::NONE;
109142
assert!(none_cell.is_none());
110143
assert!(!none_cell.is_some());
111144

112-
let some_cell = MemoryCell::from_u64(42);
145+
// A cell with a value (even zero) should be some.
146+
let some_cell = MemoryCell::from(MemoryValue::<F>::Int(F::from_u64(42)));
113147
assert!(!some_cell.is_none());
114148
assert!(some_cell.is_some());
149+
150+
let zero_cell = MemoryCell::from(MemoryValue::<F>::Int(F::ZERO));
151+
assert!(!zero_cell.is_none());
152+
assert!(zero_cell.is_some());
115153
}
116154

117155
#[test]
118-
fn test_mark_accessed_and_is_accessed() {
119-
let mut cell = MemoryCell::from_u64(7);
156+
fn test_mark_and_check_accessed() {
157+
let mut cell = MemoryCell::from(MemoryValue::<F>::Int(F::from_u64(99)));
158+
159+
// Initially not accessed.
120160
assert!(!cell.is_accessed());
161+
162+
// Mark it as accessed.
121163
cell.mark_accessed();
164+
165+
// Now it should be accessed.
122166
assert!(cell.is_accessed());
167+
// Should not affect the NONE flag.
168+
assert!(cell.is_some());
123169

124-
// Ensure value bits are still preserved
125-
assert_eq!(*cell & 0x3FFF_FFFF_FFFF_FFFF, 7);
170+
// The original value should be preserved alongside the flag.
171+
let value_without_flags = cell.0 & MemoryCell::VALUE_MASK;
172+
assert_eq!(value_without_flags, 99);
126173
}
127174

128175
#[test]
129-
fn test_none_and_access_bits_do_not_conflict() {
130-
let mut cell = MemoryCell::NONE;
131-
assert!(cell.is_none());
132-
assert!(!cell.is_accessed());
176+
fn test_flag_interactions() {
177+
// Mark a NONE cell as accessed
178+
let mut none_cell = MemoryCell::NONE;
179+
none_cell.mark_accessed();
180+
assert!(none_cell.is_none(), "is_none should be true after access");
181+
assert!(none_cell.is_accessed(), "is_accessed should be true");
182+
assert_eq!(none_cell.0, MemoryCell::NONE_MASK | MemoryCell::ACCESS_MASK);
133183

134-
// Mark accessed should not clear the NONE flag
135-
cell.mark_accessed();
136-
assert!(cell.is_none());
137-
assert!(cell.is_accessed());
184+
// Mark an ADDRESS cell as accessed
185+
let mut addr_cell = MemoryCell::from(MemoryValue::<F>::Address(MemoryAddress {
186+
segment_index: 1,
187+
offset: 2,
188+
}));
189+
addr_cell.mark_accessed();
190+
assert!(addr_cell.is_some(), "Address cell should be 'some'");
191+
assert!(
192+
(addr_cell.0 & MemoryCell::ADDRESS_MASK) != 0,
193+
"Address flag should be set"
194+
);
195+
assert!(
196+
addr_cell.is_accessed(),
197+
"Address cell should be marked accessed"
198+
);
199+
}
200+
201+
#[test]
202+
fn test_value_method() {
203+
// Test on a NONE cell.
204+
let none_cell = MemoryCell::NONE;
205+
assert_eq!(none_cell.value::<F>(), None);
206+
207+
// Test on a valid integer cell.
208+
let int_val = MemoryValue::Int(F::from_u64(123));
209+
let int_cell = MemoryCell::from(int_val.clone());
210+
assert_eq!(int_cell.value(), Some(int_val));
211+
212+
// Test on a valid address cell.
213+
let addr_val = MemoryValue::<F>::Address(MemoryAddress {
214+
segment_index: 5,
215+
offset: 10,
216+
});
217+
let addr_cell = MemoryCell::from(addr_val.clone());
218+
assert_eq!(addr_cell.value(), Some(addr_val));
219+
}
220+
221+
#[test]
222+
fn test_conversion_from_int_value() {
223+
let val = MemoryValue::Int(F::from_u64(500));
224+
let cell = MemoryCell::from(val);
225+
// Should just be the raw value, no flags set.
226+
assert_eq!(cell.0, 500);
227+
}
228+
229+
#[test]
230+
fn test_conversion_from_address_value() {
231+
let val = MemoryValue::<F>::Address(MemoryAddress {
232+
segment_index: 10,
233+
offset: 20,
234+
});
235+
let cell = MemoryCell::from(val);
236+
237+
// Expected packed value: 0x2000000A00000014
238+
// Bit 61 (ADDRESS_MASK) + segment 10 shifted by 32 + offset 20
239+
let expected = (10u64 << 32) | 20u64 | MemoryCell::ADDRESS_MASK;
240+
assert_eq!(cell.0, expected);
241+
}
242+
243+
#[test]
244+
fn test_conversion_to_int_value() {
245+
// Raw u64 for an integer.
246+
let int_cell = MemoryCell(42);
247+
let val = MemoryValue::<F>::from(int_cell);
248+
assert_eq!(val, MemoryValue::Int(F::from_u64(42)));
249+
250+
// An integer cell can also be marked accessed; the flag should be ignored.
251+
let accessed_int_cell = MemoryCell(42 | MemoryCell::ACCESS_MASK);
252+
let accessed_val = MemoryValue::<F>::from(accessed_int_cell);
253+
assert_eq!(accessed_val, MemoryValue::Int(F::from_u64(42)));
254+
}
255+
256+
#[test]
257+
fn test_conversion_to_address_value() {
258+
let raw_addr = (50u64 << 32) | 100u64 | MemoryCell::ADDRESS_MASK;
259+
let addr_cell = MemoryCell(raw_addr);
260+
let val = MemoryValue::<F>::from(addr_cell);
261+
262+
let expected = MemoryValue::Address(MemoryAddress {
263+
segment_index: 50,
264+
offset: 100,
265+
});
266+
assert_eq!(val, expected);
267+
}
268+
269+
proptest! {
270+
#[test]
271+
fn proptest_roundtrip_conversion(
272+
val in any::<MemoryValue<F>>()
273+
) {
274+
// Convert the generated MemoryValue to a MemoryCell.
275+
let cell = MemoryCell::from(val.clone());
276+
277+
// Convert the MemoryCell back to a MemoryValue.
278+
let roundtrip_val = MemoryValue::<F>::from(cell);
279+
280+
// Assert that the original and round-tripped values are identical.
281+
prop_assert_eq!(val, roundtrip_val);
282+
}
138283
}
139284
}

crates/leanVm/src/memory/error.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::fmt::Debug;
2+
3+
use thiserror::Error;
4+
5+
use super::{address::MemoryAddress, val::MemoryValue};
6+
7+
#[derive(Debug, Eq, PartialEq, Error)]
8+
pub enum MemoryError<F>
9+
where
10+
F: Debug,
11+
{
12+
/// Error for when an operation targets a memory segment that has not been allocated.
13+
#[error(
14+
"Memory access out of bounds: cannot access segment {}, as only {} segments are allocated.",
15+
0.0,
16+
0.1
17+
)]
18+
UnallocatedSegment(Box<(usize, usize)>),
19+
20+
/// Error for attempting to overwrite an existing, different value in a memory cell, violating write-once consistency.
21+
#[error(
22+
"Write-once violation at address {:?}: cannot overwrite existing value '{:?}' with new value '{:?}'.",
23+
0.0,
24+
0.1,
25+
0.2
26+
)]
27+
InconsistentMemory(Box<(MemoryAddress, MemoryValue<F>, MemoryValue<F>)>),
28+
29+
/// Error for when a memory operation would exceed the maximum capacity of a segment vector.
30+
#[error(
31+
"Memory overflow: the requested memory address is too large and exceeds the machine's capacity."
32+
)]
33+
VecCapacityExceeded,
34+
}

0 commit comments

Comments
 (0)