Skip to main content

lion_core/state/
memory.rs

1// Copyright (C) 2026 HaiyangLi
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Lion State Memory
4//!
5//! Corresponds to: Lion/State/Memory.lean
6//!
7//! Linear memory model for WASM sandboxes and ghost state for verification.
8//! Uses 4KiB page-granularity storage for efficient sparse memory.
9
10use std::collections::BTreeMap;
11
12use crate::types::{MemAddr, PluginId, Size};
13
14/// Page size bits (4KiB pages)
15const PAGE_BITS: u32 = 12;
16/// Page size in bytes
17const PAGE_SIZE: usize = 1 << PAGE_BITS; // 4096
18
19/// Get the page number for an address.
20#[inline]
21fn page_of(addr: u64) -> u64 {
22    addr >> PAGE_BITS
23}
24
25/// Get the byte offset within a page.
26#[inline]
27fn offset_in_page(addr: u64) -> usize {
28    (addr & ((1u64 << PAGE_BITS) - 1)) as usize
29}
30
31/// Error type for memory operations
32#[derive(Debug)]
33pub enum MemoryError {
34    /// Address is out of bounds (addr, len, bounds)
35    OutOfBounds(MemAddr, Size, Size),
36    /// Double free detected
37    DoubleFree(MemAddr),
38    /// Use after free detected
39    UseAfterFree(MemAddr),
40    /// Address was never allocated
41    NotAllocated(MemAddr),
42}
43
44/// Linear memory region for a plugin (WASM model)
45///
46/// Corresponds to Lean: `structure LinearMemory`
47///
48/// INVARIANTS:
49/// - All reads from uninitialized addresses return 0
50/// - Writes only modify the specified address
51/// - Bounds are immutable after creation
52///
53/// Uses 4KiB paged storage -- only touched pages are allocated.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct LinearMemory {
56    /// Page storage (sparse: only touched pages are allocated)
57    pub(crate) pages: BTreeMap<u64, Box<[u8; PAGE_SIZE]>>,
58
59    /// Memory bounds in bytes
60    ///
61    /// Corresponds to Lean: `bounds : Nat`
62    pub(crate) bounds: Size,
63}
64
65impl LinearMemory {
66    /// Create empty linear memory with given size
67    ///
68    /// Corresponds to Lean: `def LinearMemory.empty (size : Nat) : LinearMemory`
69    pub fn empty(size: Size) -> Self {
70        LinearMemory {
71            pages: BTreeMap::new(),
72            bounds: size,
73        }
74    }
75
76    /// Check if address is within bounds
77    ///
78    /// Corresponds to Lean: `def LinearMemory.addr_in_bounds`
79    pub fn addr_in_bounds(&self, addr: MemAddr, len: Size) -> bool {
80        // Overflow-safe check
81        match addr.checked_add(len) {
82            Some(end) => end <= self.bounds,
83            None => false,
84        }
85    }
86
87    /// Read byte at address (returns 0 for uninitialized / missing pages)
88    ///
89    /// Corresponds to Lean: `def LinearMemory.read (mem : LinearMemory) (addr : MemAddr) : UInt8`
90    ///
91    /// Note: Lean uses `mem.bytes.getD addr 0` which returns 0 for missing keys.
92    pub fn read(&self, addr: MemAddr) -> u8 {
93        match self.pages.get(&page_of(addr)) {
94            Some(page) => page[offset_in_page(addr)],
95            None => 0,
96        }
97    }
98
99    /// Write byte at address
100    ///
101    /// Corresponds to Lean: `def LinearMemory.write`
102    ///
103    /// Returns the new memory state with the byte written.
104    pub fn write(&self, addr: MemAddr, val: u8) -> Self {
105        let mut new = self.clone();
106        new.write_mut(addr, val);
107        new
108    }
109
110    /// Write byte at address (mutating version)
111    ///
112    /// This is an optimization for when we own the memory.
113    pub fn write_mut(&mut self, addr: MemAddr, val: u8) {
114        let pg = page_of(addr);
115        let off = offset_in_page(addr);
116        let page = self
117            .pages
118            .entry(pg)
119            .or_insert_with(|| Box::new([0u8; PAGE_SIZE]));
120        page[off] = val;
121    }
122
123    /// Get the memory bounds
124    #[inline]
125    pub fn bounds(&self) -> Size {
126        self.bounds
127    }
128
129    /// Get the number of allocated bytes (pages * PAGE_SIZE)
130    #[inline]
131    pub fn written_bytes(&self) -> usize {
132        self.pages.len() * PAGE_SIZE
133    }
134
135    /// Check if memory is empty (no pages allocated)
136    #[inline]
137    pub fn is_empty(&self) -> bool {
138        self.pages.is_empty()
139    }
140
141    /// Read a range of bytes
142    ///
143    /// Returns Err if the range would exceed bounds.
144    ///
145    /// # Errors
146    ///
147    /// Returns `MemoryError::OutOfBounds` if the address plus length exceeds the memory bounds.
148    pub fn read_range(&self, addr: MemAddr, len: Size) -> Result<Vec<u8>, MemoryError> {
149        if !self.addr_in_bounds(addr, len) {
150            return Err(MemoryError::OutOfBounds(addr, len, self.bounds));
151        }
152
153        let mut result = Vec::with_capacity(len as usize);
154        let mut pos = addr;
155        let end = addr + len;
156        while pos < end {
157            let pg = page_of(pos);
158            let off = offset_in_page(pos);
159            // How many bytes left in this page?
160            let page_remaining = PAGE_SIZE - off;
161            let needed = (end - pos) as usize;
162            let chunk = page_remaining.min(needed);
163
164            match self.pages.get(&pg) {
165                Some(page) => result.extend_from_slice(&page[off..off + chunk]),
166                None => result.resize(result.len() + chunk, 0),
167            }
168            pos += chunk as u64;
169        }
170        Ok(result)
171    }
172
173    /// Write a range of bytes
174    ///
175    /// Returns the new memory state with all bytes written.
176    ///
177    /// # Errors
178    ///
179    /// Returns `MemoryError::OutOfBounds` if the address plus data length exceeds the memory bounds.
180    pub fn write_range(&self, addr: MemAddr, data: &[u8]) -> Result<Self, MemoryError> {
181        let len = data.len() as Size;
182        if !self.addr_in_bounds(addr, len) {
183            return Err(MemoryError::OutOfBounds(addr, len, self.bounds));
184        }
185
186        let mut new = self.clone();
187        new.write_range_mut(addr, data);
188        Ok(new)
189    }
190
191    /// Write a range of bytes (mutating version)
192    pub fn write_range_mut(&mut self, addr: MemAddr, data: &[u8]) {
193        let mut pos = addr;
194        let mut src_off = 0usize;
195        while src_off < data.len() {
196            let pg = page_of(pos);
197            let off = offset_in_page(pos);
198            let page_remaining = PAGE_SIZE - off;
199            let remaining_data = data.len() - src_off;
200            let chunk = page_remaining.min(remaining_data);
201
202            let page = self
203                .pages
204                .entry(pg)
205                .or_insert_with(|| Box::new([0u8; PAGE_SIZE]));
206            page[off..off + chunk].copy_from_slice(&data[src_off..src_off + chunk]);
207
208            pos += chunk as u64;
209            src_off += chunk;
210        }
211    }
212}
213
214impl Default for LinearMemory {
215    /// Default: empty memory with zero bounds
216    fn default() -> Self {
217        LinearMemory::empty(0)
218    }
219}
220
221/// Resource lifecycle status
222///
223/// Corresponds to Lean: `inductive ResourceStatus`
224#[derive(Debug, Clone, PartialEq, Eq, Default)]
225pub enum ResourceStatus {
226    /// Resource has not been allocated
227    ///
228    /// Corresponds to Lean: `| unallocated`
229    #[default]
230    Unallocated,
231
232    /// Resource is allocated with an owner
233    ///
234    /// Corresponds to Lean: `| allocated (owner : PluginId)`
235    Allocated {
236        /// The plugin that owns this resource
237        owner: PluginId,
238    },
239
240    /// Resource has been freed
241    ///
242    /// Corresponds to Lean: `| freed`
243    Freed,
244}
245
246impl ResourceStatus {
247    /// Check if the resource is allocated
248    #[inline]
249    pub fn is_allocated(&self) -> bool {
250        matches!(self, ResourceStatus::Allocated { .. })
251    }
252
253    /// Check if the resource is freed
254    #[inline]
255    pub fn is_freed(&self) -> bool {
256        matches!(self, ResourceStatus::Freed)
257    }
258
259    /// Check if the resource is unallocated
260    #[inline]
261    pub fn is_unallocated(&self) -> bool {
262        matches!(self, ResourceStatus::Unallocated)
263    }
264
265    /// Get the owner if allocated
266    #[inline]
267    pub fn owner(&self) -> Option<PluginId> {
268        match self {
269            ResourceStatus::Allocated { owner } => Some(*owner),
270            _ => None,
271        }
272    }
273}
274
275/// Ghost state for verification (not runtime)
276///
277/// Corresponds to Lean: `structure MetaState`
278///
279/// This tracks resource lifecycle for proofs about temporal safety.
280/// At runtime, only the freed_set is used for double-free prevention.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct MetaState {
283    /// Resource status tracking
284    ///
285    /// Corresponds to Lean: `resources : Std.HashMap MemAddr ResourceStatus`
286    /// NOTE: Using BTreeMap for deterministic iteration.
287    pub(crate) resources: BTreeMap<MemAddr, ResourceStatus>,
288
289    /// Set of freed addresses
290    ///
291    /// Corresponds to Lean: `freed_set : Finset MemAddr`
292    /// NOTE: Using Vec with sorted uniqueness.
293    pub(crate) freed_set: Vec<MemAddr>,
294}
295
296impl MetaState {
297    /// Create empty meta state
298    ///
299    /// Corresponds to Lean: `def MetaState.empty : MetaState`
300    pub fn empty() -> Self {
301        MetaState {
302            resources: BTreeMap::new(),
303            freed_set: Vec::new(),
304        }
305    }
306
307    /// Helper: check if addr is in freed_set
308    fn is_freed_addr(&self, addr: MemAddr) -> bool {
309        self.freed_set.binary_search(&addr).is_ok()
310    }
311
312    /// Helper: insert addr into freed_set (sorted uniqueness)
313    fn insert_freed_addr(freed_set: &mut Vec<MemAddr>, addr: MemAddr) {
314        match freed_set.binary_search(&addr) {
315            Ok(_) => {} // Already present
316            Err(pos) => freed_set.insert(pos, addr),
317        }
318    }
319
320    /// Check if resource is live (allocated and not freed)
321    ///
322    /// Corresponds to Lean: `def MetaState.is_live (ms : MetaState) (addr : MemAddr) : Prop`
323    pub fn is_live(&self, addr: MemAddr) -> bool {
324        match self.resources.get(&addr) {
325            Some(ResourceStatus::Allocated { .. }) => !self.is_freed_addr(addr),
326            _ => false,
327        }
328    }
329
330    /// Mark resource as allocated
331    ///
332    /// Corresponds to Lean: `def MetaState.alloc`
333    ///
334    /// Returns the new MetaState with the resource marked as allocated.
335    pub fn alloc(&self, addr: MemAddr, owner: PluginId) -> Self {
336        let mut new_resources = self.resources.clone();
337        new_resources.insert(addr, ResourceStatus::Allocated { owner });
338        MetaState {
339            resources: new_resources,
340            freed_set: self.freed_set.clone(),
341        }
342    }
343
344    /// Mark resource as freed
345    ///
346    /// Corresponds to Lean: `def MetaState.free`
347    ///
348    /// **Data Refinement Note (from Lean)**: The `resources` field is NOT updated.
349    /// Only `freed_set` is used for double-free prevention.
350    pub fn free(&self, addr: MemAddr) -> Self {
351        let mut new_freed_set = self.freed_set.clone();
352        Self::insert_freed_addr(&mut new_freed_set, addr);
353        MetaState {
354            resources: self.resources.clone(),
355            freed_set: new_freed_set,
356        }
357    }
358
359    /// Mark resource as allocated (mutating version)
360    pub fn alloc_mut(&mut self, addr: MemAddr, owner: PluginId) {
361        self.resources
362            .insert(addr, ResourceStatus::Allocated { owner });
363    }
364
365    /// Mark resource as freed (mutating version)
366    ///
367    /// Requires the resource to be currently allocated. Updates the resource
368    /// status to `Freed` and adds the address to the freed set.
369    ///
370    /// # Errors
371    ///
372    /// Returns `MemoryError::DoubleFree` if the address has already been freed.
373    /// Returns `MemoryError::NotAllocated` if the address was never allocated.
374    pub fn free_mut(&mut self, addr: MemAddr) -> Result<(), MemoryError> {
375        if self.is_freed_addr(addr) {
376            return Err(MemoryError::DoubleFree(addr));
377        }
378
379        match self.resources.get_mut(&addr) {
380            Some(status @ ResourceStatus::Allocated { .. }) => {
381                *status = ResourceStatus::Freed;
382                Self::insert_freed_addr(&mut self.freed_set, addr);
383                Ok(())
384            }
385            Some(ResourceStatus::Freed) => Err(MemoryError::DoubleFree(addr)),
386            _ => Err(MemoryError::NotAllocated(addr)),
387        }
388    }
389
390    /// Get the number of allocated resources
391    pub fn allocated_count(&self) -> usize {
392        self.resources.values().filter(|s| s.is_allocated()).count()
393    }
394
395    /// Get the number of freed resources
396    #[inline]
397    pub fn freed_count(&self) -> usize {
398        self.freed_set.len()
399    }
400
401    /// Get the number of resources in the map
402    #[inline]
403    pub fn resource_count(&self) -> usize {
404        self.resources.len()
405    }
406
407    /// Check if an address has been freed
408    #[inline]
409    pub fn is_freed(&self, addr: MemAddr) -> bool {
410        self.freed_set.binary_search(&addr).is_ok()
411    }
412
413    /// Get the status of a resource
414    #[inline]
415    pub fn get_status(&self, addr: MemAddr) -> Option<&ResourceStatus> {
416        self.resources.get(&addr)
417    }
418}
419
420impl Default for MetaState {
421    fn default() -> Self {
422        MetaState::empty()
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_linear_memory_empty() {
432        let mem = LinearMemory::empty(1024);
433        assert_eq!(mem.bounds(), 1024);
434        assert!(mem.is_empty());
435    }
436
437    #[test]
438    fn test_linear_memory_read_uninitialized() {
439        let mem = LinearMemory::empty(1024);
440        // All uninitialized reads return 0
441        assert_eq!(mem.read(0), 0);
442        assert_eq!(mem.read(100), 0);
443        assert_eq!(mem.read(1023), 0);
444    }
445
446    #[test]
447    fn test_linear_memory_write_read() {
448        let mem = LinearMemory::empty(1024);
449        let mem = mem.write(100, 42);
450        assert_eq!(mem.read(100), 42);
451        // Other addresses still 0
452        assert_eq!(mem.read(99), 0);
453        assert_eq!(mem.read(101), 0);
454    }
455
456    #[test]
457    fn test_linear_memory_bounds_check() {
458        let mem = LinearMemory::empty(1024);
459        assert!(mem.addr_in_bounds(0, 1024));
460        assert!(mem.addr_in_bounds(1023, 1));
461        assert!(!mem.addr_in_bounds(1024, 1));
462        assert!(!mem.addr_in_bounds(0, 1025));
463        // Overflow protection
464        assert!(!mem.addr_in_bounds(u64::MAX, 1));
465    }
466
467    #[test]
468    fn test_linear_memory_read_range() {
469        let mem = LinearMemory::empty(1024);
470        let mem = mem.write(10, 1);
471        let mem = mem.write(11, 2);
472        let mem = mem.write(12, 3);
473
474        let result = mem.read_range(10, 3);
475        assert!(result.is_ok());
476        assert_eq!(result.ok(), Some(vec![1, 2, 3]));
477    }
478
479    #[test]
480    fn test_linear_memory_read_range_out_of_bounds() {
481        let mem = LinearMemory::empty(100);
482        let result = mem.read_range(90, 20);
483        assert!(matches!(result, Err(MemoryError::OutOfBounds(_, _, _))));
484    }
485
486    #[test]
487    fn test_resource_status() {
488        let unalloc = ResourceStatus::Unallocated;
489        assert!(unalloc.is_unallocated());
490        assert!(!unalloc.is_allocated());
491        assert!(!unalloc.is_freed());
492        assert_eq!(unalloc.owner(), None);
493
494        let alloc = ResourceStatus::Allocated { owner: 42 };
495        assert!(!alloc.is_unallocated());
496        assert!(alloc.is_allocated());
497        assert!(!alloc.is_freed());
498        assert_eq!(alloc.owner(), Some(42));
499
500        let freed = ResourceStatus::Freed;
501        assert!(!freed.is_unallocated());
502        assert!(!freed.is_allocated());
503        assert!(freed.is_freed());
504        assert_eq!(freed.owner(), None);
505    }
506
507    #[test]
508    fn test_meta_state_empty() {
509        let ms = MetaState::empty();
510        assert_eq!(ms.allocated_count(), 0);
511        assert_eq!(ms.freed_count(), 0);
512        assert!(!ms.is_live(0));
513    }
514
515    #[test]
516    fn test_meta_state_alloc_makes_live() {
517        let ms = MetaState::empty();
518        let ms = ms.alloc(100, 1);
519        assert!(ms.is_live(100));
520        assert_eq!(ms.allocated_count(), 1);
521    }
522
523    #[test]
524    fn test_meta_state_free_makes_not_live() {
525        let ms = MetaState::empty();
526        let ms = ms.alloc(100, 1);
527        assert!(ms.is_live(100));
528
529        let ms = ms.free(100);
530        assert!(!ms.is_live(100));
531        assert!(ms.is_freed(100));
532    }
533
534    #[test]
535    fn test_meta_state_alloc_preserves_live() {
536        // Corresponds to Lean theorem: alloc_preserves_is_live
537        let ms = MetaState::empty();
538        let ms = ms.alloc(100, 1);
539        let ms = ms.alloc(200, 2);
540
541        // Both should be live
542        assert!(ms.is_live(100));
543        assert!(ms.is_live(200));
544    }
545
546    #[test]
547    fn test_meta_state_free_preserves_live() {
548        // Corresponds to Lean theorem: free_preserves_is_live
549        let ms = MetaState::empty();
550        let ms = ms.alloc(100, 1);
551        let ms = ms.alloc(200, 2);
552
553        // Free one
554        let ms = ms.free(100);
555
556        // 100 not live, 200 still live
557        assert!(!ms.is_live(100));
558        assert!(ms.is_live(200));
559    }
560
561    #[test]
562    fn test_meta_state_double_free_detection() {
563        let mut ms = MetaState::empty();
564        ms.alloc_mut(100, 1);
565
566        // First free succeeds
567        assert!(ms.free_mut(100).is_ok());
568
569        // Second free fails
570        let result = ms.free_mut(100);
571        assert!(matches!(result, Err(MemoryError::DoubleFree(100))));
572    }
573
574    #[test]
575    fn test_linear_memory_page_spanning_write_read() {
576        // Write data that spans a page boundary
577        let mem = LinearMemory::empty(8192);
578        let boundary = PAGE_SIZE as u64 - 2; // 2 bytes before page boundary
579        let data = vec![0xAA, 0xBB, 0xCC, 0xDD]; // 4 bytes spanning boundary
580        let mem = mem.write_range(boundary, &data).unwrap();
581
582        let read_back = mem.read_range(boundary, 4).unwrap();
583        assert_eq!(read_back, data);
584
585        // Verify individual bytes
586        assert_eq!(mem.read(boundary), 0xAA);
587        assert_eq!(mem.read(boundary + 1), 0xBB);
588        assert_eq!(mem.read(boundary + 2), 0xCC); // on next page
589        assert_eq!(mem.read(boundary + 3), 0xDD);
590    }
591}