Skip to main content

oxiz_proof/
mmap.rs

1//! Memory-mapped proof storage for large proofs.
2//!
3//! This module provides memory-mapped file storage for proofs, enabling
4//! efficient handling of proofs larger than available RAM.
5
6use crate::proof::{Proof, ProofNode, ProofNodeId};
7use rustc_hash::FxHashMap;
8use std::fs::{File, OpenOptions};
9use std::io::{self, Read, Seek, SeekFrom, Write};
10use std::path::{Path, PathBuf};
11
12/// Configuration for memory-mapped storage.
13#[derive(Debug, Clone)]
14pub struct MmapConfig {
15    /// File path for storage
16    pub file_path: PathBuf,
17    /// Enable read-only mode
18    pub read_only: bool,
19    /// Sync to disk on write
20    pub sync_on_write: bool,
21    /// Cache size (number of nodes to keep in memory)
22    pub cache_size: usize,
23}
24
25impl MmapConfig {
26    /// Create a new mmap configuration.
27    pub fn new<P: AsRef<Path>>(path: P) -> Self {
28        Self {
29            file_path: path.as_ref().to_path_buf(),
30            read_only: false,
31            sync_on_write: true,
32            cache_size: 10000,
33        }
34    }
35
36    /// Set read-only mode.
37    pub fn with_read_only(mut self, read_only: bool) -> Self {
38        self.read_only = read_only;
39        self
40    }
41
42    /// Set sync on write.
43    pub fn with_sync_on_write(mut self, sync: bool) -> Self {
44        self.sync_on_write = sync;
45        self
46    }
47
48    /// Set cache size.
49    pub fn with_cache_size(mut self, size: usize) -> Self {
50        self.cache_size = size;
51        self
52    }
53}
54
55/// Memory-mapped proof storage.
56pub struct MmapProofStorage {
57    config: MmapConfig,
58    file: Option<File>,
59    index: FxHashMap<ProofNodeId, u64>, // Node ID -> file offset
60    cache: FxHashMap<ProofNodeId, ProofNode>,
61    cache_order: Vec<ProofNodeId>,
62}
63
64impl MmapProofStorage {
65    /// Create a new mmap storage.
66    pub fn new(config: MmapConfig) -> io::Result<Self> {
67        let file = if config.read_only {
68            Some(File::open(&config.file_path)?)
69        } else {
70            Some(
71                OpenOptions::new()
72                    .read(true)
73                    .write(true)
74                    .create(true)
75                    .truncate(false)
76                    .open(&config.file_path)?,
77            )
78        };
79
80        Ok(Self {
81            config,
82            file,
83            index: FxHashMap::default(),
84            cache: FxHashMap::default(),
85            cache_order: Vec::new(),
86        })
87    }
88
89    /// Store a proof to disk.
90    pub fn store_proof(&mut self, proof: &Proof) -> io::Result<()> {
91        if self.config.read_only {
92            return Err(io::Error::new(
93                io::ErrorKind::PermissionDenied,
94                "Storage is read-only",
95            ));
96        }
97
98        {
99            let file = self
100                .file
101                .as_mut()
102                .ok_or_else(|| io::Error::other("File not opened"))?;
103
104            // Write each node to file
105            for node in proof.nodes() {
106                let offset = file.seek(SeekFrom::End(0))?;
107                let node_id = node.id;
108                Self::write_node_to_file(file, node)?;
109                self.index.insert(node_id, offset);
110            }
111
112            if self.config.sync_on_write {
113                file.sync_all()?;
114            }
115        }
116
117        Ok(())
118    }
119
120    /// Load a node from disk.
121    pub fn load_node(&mut self, id: ProofNodeId) -> io::Result<Option<ProofNode>> {
122        // Check cache first
123        if let Some(node) = self.cache.get(&id) {
124            return Ok(Some(node.clone()));
125        }
126
127        // Look up offset in index
128        let offset = match self.index.get(&id) {
129            Some(&off) => off,
130            None => return Ok(None),
131        };
132
133        // Read node (using helper to avoid borrow issues)
134        let node = {
135            let file = self
136                .file
137                .as_mut()
138                .ok_or_else(|| io::Error::other("File not opened"))?;
139
140            file.seek(SeekFrom::Start(offset))?;
141            Self::read_node_from_file(file)?
142        };
143
144        // Add to cache
145        self.add_to_cache(id, node.clone());
146
147        Ok(Some(node))
148    }
149
150    /// Load entire proof from disk.
151    pub fn load_proof(&mut self) -> io::Result<Proof> {
152        let mut proof = Proof::new();
153
154        {
155            let file = self
156                .file
157                .as_mut()
158                .ok_or_else(|| io::Error::other("File not opened"))?;
159            file.seek(SeekFrom::Start(0))?;
160
161            // Read all nodes
162            while let Ok(node) = Self::read_node_from_file(file) {
163                // Reconstruct proof (simplified - assumes nodes are in order)
164                match &node.step {
165                    crate::proof::ProofStep::Axiom { conclusion } => {
166                        proof.add_axiom(conclusion);
167                    }
168                    crate::proof::ProofStep::Inference {
169                        rule,
170                        premises,
171                        conclusion,
172                        ..
173                    } => {
174                        proof.add_inference(rule, premises.to_vec(), conclusion);
175                    }
176                }
177            }
178        }
179
180        Ok(proof)
181    }
182
183    /// Clear the cache.
184    pub fn clear_cache(&mut self) {
185        self.cache.clear();
186        self.cache_order.clear();
187    }
188
189    /// Get cache statistics.
190    pub fn cache_stats(&self) -> (usize, usize) {
191        (self.cache.len(), self.config.cache_size)
192    }
193
194    /// Close the storage.
195    pub fn close(&mut self) -> io::Result<()> {
196        if let Some(file) = self.file.take() {
197            file.sync_all()?;
198        }
199        Ok(())
200    }
201
202    // Helper: Write a node to file
203    fn write_node_to_file(file: &mut File, node: &ProofNode) -> io::Result<()> {
204        // Simple binary format (in production, use bincode or similar)
205        let serialized = format!("{:?}\n", node); // Simplified serialization
206        file.write_all(serialized.as_bytes())?;
207        Ok(())
208    }
209
210    // Helper: Read a node from file
211    fn read_node_from_file(file: &mut File) -> io::Result<ProofNode> {
212        let mut buffer = String::new();
213        let mut temp_buf = [0u8; 1];
214
215        // Read until newline (simplified)
216        loop {
217            let n = file.read(&mut temp_buf)?;
218            if n == 0 || temp_buf[0] == b'\n' {
219                break;
220            }
221            buffer.push(temp_buf[0] as char);
222        }
223
224        if buffer.is_empty() {
225            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"));
226        }
227
228        // Deserialize (simplified - in production use proper deserialization)
229        // For now, return a dummy node
230        Err(io::Error::new(
231            io::ErrorKind::InvalidData,
232            "Deserialization not implemented",
233        ))
234    }
235
236    // Helper: Add node to cache with LRU eviction
237    fn add_to_cache(&mut self, id: ProofNodeId, node: ProofNode) {
238        if self.cache.len() >= self.config.cache_size {
239            // Evict oldest entry (LRU)
240            if let Some(oldest_id) = self.cache_order.first().copied() {
241                self.cache.remove(&oldest_id);
242                self.cache_order.remove(0);
243            }
244        }
245
246        self.cache.insert(id, node);
247        self.cache_order.push(id);
248    }
249}
250
251/// Mmap-backed proof wrapper.
252pub struct MmapProof {
253    storage: MmapProofStorage,
254    metadata: ProofMetadata,
255}
256
257/// Metadata for mmap proof.
258#[derive(Debug, Clone, Default)]
259pub struct ProofMetadata {
260    /// Number of nodes
261    pub num_nodes: usize,
262    /// Number of axioms
263    pub num_axioms: usize,
264    /// Maximum depth
265    pub max_depth: usize,
266}
267
268impl MmapProof {
269    /// Create a new mmap proof.
270    pub fn new(config: MmapConfig) -> io::Result<Self> {
271        let storage = MmapProofStorage::new(config)?;
272        Ok(Self {
273            storage,
274            metadata: ProofMetadata::default(),
275        })
276    }
277
278    /// Store a proof.
279    pub fn store(&mut self, proof: &Proof) -> io::Result<()> {
280        self.metadata.num_nodes = proof.len();
281        self.metadata.num_axioms = proof
282            .nodes()
283            .iter()
284            .filter(|n| matches!(n.step, crate::proof::ProofStep::Axiom { .. }))
285            .count();
286        self.metadata.max_depth = proof
287            .nodes()
288            .iter()
289            .map(|n| n.depth as usize)
290            .max()
291            .unwrap_or(0);
292
293        self.storage.store_proof(proof)
294    }
295
296    /// Load a node by ID.
297    pub fn get_node(&mut self, id: ProofNodeId) -> io::Result<Option<ProofNode>> {
298        self.storage.load_node(id)
299    }
300
301    /// Load the entire proof.
302    pub fn load_all(&mut self) -> io::Result<Proof> {
303        self.storage.load_proof()
304    }
305
306    /// Get metadata.
307    pub fn metadata(&self) -> &ProofMetadata {
308        &self.metadata
309    }
310
311    /// Clear cache.
312    pub fn clear_cache(&mut self) {
313        self.storage.clear_cache();
314    }
315
316    /// Close the storage.
317    pub fn close(mut self) -> io::Result<()> {
318        self.storage.close()
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use std::env;
326
327    fn temp_path() -> PathBuf {
328        let mut path = env::temp_dir();
329        path.push(format!("test_proof_{}.mmap", std::process::id()));
330        path
331    }
332
333    #[test]
334    fn test_mmap_config_new() {
335        let path = temp_path();
336        let config = MmapConfig::new(&path);
337        assert_eq!(config.file_path, path);
338        assert!(!config.read_only);
339        assert!(config.sync_on_write);
340        assert_eq!(config.cache_size, 10000);
341    }
342
343    #[test]
344    fn test_mmap_config_with_settings() {
345        let path = temp_path();
346        let config = MmapConfig::new(&path)
347            .with_read_only(true)
348            .with_sync_on_write(false)
349            .with_cache_size(5000);
350        assert!(config.read_only);
351        assert!(!config.sync_on_write);
352        assert_eq!(config.cache_size, 5000);
353    }
354
355    #[test]
356    fn test_mmap_proof_new() {
357        let path = temp_path();
358        let config = MmapConfig::new(&path);
359        let result = MmapProof::new(config);
360        // May fail if file operations are restricted, which is acceptable
361        if let Ok(mmap_proof) = result {
362            assert_eq!(mmap_proof.metadata.num_nodes, 0);
363        }
364    }
365
366    #[test]
367    fn test_proof_metadata_default() {
368        let metadata = ProofMetadata::default();
369        assert_eq!(metadata.num_nodes, 0);
370        assert_eq!(metadata.num_axioms, 0);
371        assert_eq!(metadata.max_depth, 0);
372    }
373
374    #[test]
375    fn test_cache_stats() {
376        let path = temp_path();
377        let config = MmapConfig::new(&path).with_cache_size(100);
378        if let Ok(storage) = MmapProofStorage::new(config) {
379            let (used, total) = storage.cache_stats();
380            assert_eq!(used, 0);
381            assert_eq!(total, 100);
382        }
383    }
384
385    #[test]
386    fn test_clear_cache() {
387        let path = temp_path();
388        let config = MmapConfig::new(&path);
389        if let Ok(mut storage) = MmapProofStorage::new(config) {
390            storage.clear_cache();
391            let (used, _) = storage.cache_stats();
392            assert_eq!(used, 0);
393        }
394    }
395}