Skip to main content

oxiz_proof/
streaming.rs

1//! Proof streaming for processing large proofs efficiently.
2//!
3//! This module provides streaming APIs to process large proofs without
4//! loading the entire proof into memory at once.
5
6use crate::proof::{Proof, ProofNode, ProofNodeId, ProofStep};
7use rustc_hash::FxHashMap;
8use std::io::{self, BufWriter, Write};
9
10/// Configuration for proof streaming.
11#[derive(Debug, Clone)]
12pub struct StreamConfig {
13    /// Chunk size for streaming (number of nodes)
14    pub chunk_size: usize,
15    /// Buffer size for I/O operations
16    pub buffer_size: usize,
17    /// Enable compression during streaming
18    pub compress: bool,
19}
20
21impl Default for StreamConfig {
22    fn default() -> Self {
23        Self {
24            chunk_size: 1000,
25            buffer_size: 8192,
26            compress: false,
27        }
28    }
29}
30
31impl StreamConfig {
32    /// Create a new stream configuration.
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Set the chunk size.
38    pub fn with_chunk_size(mut self, size: usize) -> Self {
39        self.chunk_size = size;
40        self
41    }
42
43    /// Set the buffer size.
44    pub fn with_buffer_size(mut self, size: usize) -> Self {
45        self.buffer_size = size;
46        self
47    }
48
49    /// Enable compression.
50    pub fn with_compression(mut self, enabled: bool) -> Self {
51        self.compress = enabled;
52        self
53    }
54}
55
56/// Chunk of proof nodes for streaming.
57#[derive(Debug, Clone)]
58pub struct ProofChunk {
59    /// Starting node ID in this chunk
60    pub start_id: ProofNodeId,
61    /// Nodes in this chunk
62    pub nodes: Vec<ProofNode>,
63    /// Chunk index
64    pub index: usize,
65    /// Total number of chunks
66    pub total_chunks: usize,
67}
68
69impl ProofChunk {
70    /// Create a new proof chunk.
71    pub fn new(
72        start_id: ProofNodeId,
73        nodes: Vec<ProofNode>,
74        index: usize,
75        total_chunks: usize,
76    ) -> Self {
77        Self {
78            start_id,
79            nodes,
80            index,
81            total_chunks,
82        }
83    }
84
85    /// Get the number of nodes in this chunk.
86    pub fn len(&self) -> usize {
87        self.nodes.len()
88    }
89
90    /// Check if the chunk is empty.
91    pub fn is_empty(&self) -> bool {
92        self.nodes.is_empty()
93    }
94
95    /// Check if this is the last chunk.
96    pub fn is_last(&self) -> bool {
97        self.index + 1 == self.total_chunks
98    }
99}
100
101/// Streaming proof reader.
102pub struct ProofStreamer {
103    config: StreamConfig,
104}
105
106impl Default for ProofStreamer {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl ProofStreamer {
113    /// Create a new proof streamer.
114    pub fn new() -> Self {
115        Self {
116            config: StreamConfig::default(),
117        }
118    }
119
120    /// Create with custom configuration.
121    pub fn with_config(config: StreamConfig) -> Self {
122        Self { config }
123    }
124
125    /// Stream proof in chunks.
126    pub fn stream_chunks<'a>(&self, proof: &'a Proof) -> ProofChunkIterator<'a> {
127        ProofChunkIterator::new(proof, self.config.chunk_size)
128    }
129
130    /// Process proof in streaming fashion with a callback.
131    pub fn process_streaming<F>(&self, proof: &Proof, mut callback: F) -> Result<(), String>
132    where
133        F: FnMut(&ProofChunk) -> Result<(), String>,
134    {
135        for chunk in self.stream_chunks(proof) {
136            callback(&chunk)?;
137        }
138        Ok(())
139    }
140
141    /// Write proof to a writer in streaming fashion.
142    pub fn write_streaming<W: Write>(&self, proof: &Proof, writer: W) -> io::Result<()> {
143        let mut buf_writer = BufWriter::with_capacity(self.config.buffer_size, writer);
144
145        for chunk in self.stream_chunks(proof) {
146            for node in &chunk.nodes {
147                writeln!(buf_writer, "{}", self.format_node(node))?;
148            }
149        }
150
151        buf_writer.flush()
152    }
153
154    // Helper: Format a node for output
155    fn format_node(&self, node: &ProofNode) -> String {
156        match &node.step {
157            ProofStep::Axiom { conclusion } => {
158                format!("axiom {} : {}", node.id, conclusion)
159            }
160            ProofStep::Inference {
161                rule,
162                premises,
163                conclusion,
164                ..
165            } => {
166                let premise_str = premises
167                    .iter()
168                    .map(|p| p.to_string())
169                    .collect::<Vec<_>>()
170                    .join(", ");
171                format!(
172                    "infer {} : {} from [{}] => {}",
173                    node.id, rule, premise_str, conclusion
174                )
175            }
176        }
177    }
178}
179
180/// Iterator over proof chunks.
181pub struct ProofChunkIterator<'a> {
182    proof: &'a Proof,
183    chunk_size: usize,
184    current_index: usize,
185    total_nodes: usize,
186}
187
188impl<'a> ProofChunkIterator<'a> {
189    /// Create a new chunk iterator.
190    pub fn new(proof: &'a Proof, chunk_size: usize) -> Self {
191        Self {
192            proof,
193            chunk_size,
194            current_index: 0,
195            total_nodes: proof.len(),
196        }
197    }
198}
199
200impl<'a> Iterator for ProofChunkIterator<'a> {
201    type Item = ProofChunk;
202
203    fn next(&mut self) -> Option<Self::Item> {
204        if self.current_index >= self.total_nodes {
205            return None;
206        }
207
208        let start_idx = self.current_index;
209        let end_idx = (start_idx + self.chunk_size).min(self.total_nodes);
210
211        let nodes: Vec<ProofNode> = self.proof.nodes()[start_idx..end_idx].to_vec();
212
213        let total_chunks = self.total_nodes.div_ceil(self.chunk_size);
214        let chunk_index = start_idx / self.chunk_size;
215
216        let start_id = if !nodes.is_empty() {
217            nodes[0].id
218        } else {
219            ProofNodeId(0)
220        };
221
222        self.current_index = end_idx;
223
224        Some(ProofChunk::new(start_id, nodes, chunk_index, total_chunks))
225    }
226}
227
228/// Streaming proof builder for incremental construction.
229pub struct StreamingProofBuilder {
230    /// Accumulated proof nodes
231    nodes: Vec<ProofNode>,
232    /// Node ID mapping
233    node_map: FxHashMap<ProofNodeId, usize>,
234    /// Current proof
235    proof: Proof,
236    /// Stream configuration (reserved for future use)
237    #[allow(dead_code)]
238    config: StreamConfig,
239}
240
241impl Default for StreamingProofBuilder {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl StreamingProofBuilder {
248    /// Create a new streaming proof builder.
249    pub fn new() -> Self {
250        Self {
251            nodes: Vec::new(),
252            node_map: FxHashMap::default(),
253            proof: Proof::new(),
254            config: StreamConfig::default(),
255        }
256    }
257
258    /// Create with custom configuration.
259    pub fn with_config(config: StreamConfig) -> Self {
260        Self {
261            nodes: Vec::new(),
262            node_map: FxHashMap::default(),
263            proof: Proof::new(),
264            config,
265        }
266    }
267
268    /// Add an axiom to the stream.
269    pub fn add_axiom(&mut self, conclusion: &str) -> ProofNodeId {
270        self.proof.add_axiom(conclusion)
271    }
272
273    /// Add an inference to the stream.
274    pub fn add_inference(
275        &mut self,
276        rule: &str,
277        premises: Vec<ProofNodeId>,
278        conclusion: &str,
279    ) -> ProofNodeId {
280        self.proof.add_inference(rule, premises, conclusion)
281    }
282
283    /// Flush accumulated nodes and get the current proof.
284    pub fn flush(&mut self) -> Proof {
285        let proof = std::mem::take(&mut self.proof);
286        self.nodes.clear();
287        self.node_map.clear();
288        proof
289    }
290
291    /// Get the number of nodes in the stream.
292    pub fn len(&self) -> usize {
293        self.proof.len()
294    }
295
296    /// Check if the stream is empty.
297    pub fn is_empty(&self) -> bool {
298        self.proof.is_empty()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_stream_config_new() {
308        let config = StreamConfig::new();
309        assert_eq!(config.chunk_size, 1000);
310        assert_eq!(config.buffer_size, 8192);
311        assert!(!config.compress);
312    }
313
314    #[test]
315    fn test_stream_config_with_settings() {
316        let config = StreamConfig::new()
317            .with_chunk_size(500)
318            .with_buffer_size(4096)
319            .with_compression(true);
320        assert_eq!(config.chunk_size, 500);
321        assert_eq!(config.buffer_size, 4096);
322        assert!(config.compress);
323    }
324
325    #[test]
326    fn test_proof_chunk_new() {
327        let chunk = ProofChunk::new(ProofNodeId(0), Vec::new(), 0, 1);
328        assert_eq!(chunk.index, 0);
329        assert_eq!(chunk.total_chunks, 1);
330        assert!(chunk.is_empty());
331        assert!(chunk.is_last());
332    }
333
334    #[test]
335    fn test_proof_streamer_new() {
336        let streamer = ProofStreamer::new();
337        assert_eq!(streamer.config.chunk_size, 1000);
338    }
339
340    #[test]
341    fn test_stream_chunks() {
342        let mut proof = Proof::new();
343        for i in 0..5 {
344            proof.add_axiom(format!("axiom_{}", i));
345        }
346
347        let streamer = ProofStreamer::with_config(StreamConfig::new().with_chunk_size(2));
348        let chunks: Vec<ProofChunk> = streamer.stream_chunks(&proof).collect();
349
350        assert_eq!(chunks.len(), 3); // 5 nodes / 2 per chunk = 3 chunks
351        assert_eq!(chunks[0].len(), 2);
352        assert_eq!(chunks[1].len(), 2);
353        assert_eq!(chunks[2].len(), 1);
354    }
355
356    #[test]
357    fn test_chunk_iterator() {
358        let mut proof = Proof::new();
359        proof.add_axiom("x = x");
360        proof.add_axiom("y = y");
361
362        let mut iter = ProofChunkIterator::new(&proof, 1);
363        let chunk1 = iter.next();
364        assert!(chunk1.is_some());
365        assert_eq!(chunk1.expect("test operation should succeed").len(), 1);
366
367        let chunk2 = iter.next();
368        assert!(chunk2.is_some());
369        assert_eq!(chunk2.expect("test operation should succeed").len(), 1);
370
371        let chunk3 = iter.next();
372        assert!(chunk3.is_none());
373    }
374
375    #[test]
376    fn test_process_streaming() {
377        let mut proof = Proof::new();
378        proof.add_axiom("x = x");
379        proof.add_axiom("y = y");
380
381        let streamer = ProofStreamer::new();
382        let mut count = 0;
383        let result = streamer.process_streaming(&proof, |chunk| {
384            count += chunk.len();
385            Ok(())
386        });
387
388        assert!(result.is_ok());
389        assert_eq!(count, 2);
390    }
391
392    #[test]
393    fn test_streaming_builder() {
394        let mut builder = StreamingProofBuilder::new();
395        builder.add_axiom("x = x");
396        builder.add_axiom("y = y");
397
398        assert_eq!(builder.len(), 2);
399        assert!(!builder.is_empty());
400
401        let proof = builder.flush();
402        assert_eq!(proof.len(), 2);
403        assert_eq!(builder.len(), 0);
404    }
405
406    #[test]
407    fn test_write_streaming() {
408        let mut proof = Proof::new();
409        proof.add_axiom("x = x");
410        proof.add_axiom("y = y");
411
412        let streamer = ProofStreamer::new();
413        let mut output = Vec::new();
414        let result = streamer.write_streaming(&proof, &mut output);
415
416        assert!(result.is_ok());
417        let output_str = String::from_utf8(output).expect("test operation should succeed");
418        assert!(output_str.contains("axiom"));
419        assert!(output_str.contains("x = x"));
420    }
421}