Skip to main content

oxiz_proof/
lazy.rs

1//! Lazy proof evaluation for efficient proof processing.
2//!
3//! This module provides lazy evaluation strategies that defer computation
4//! until results are actually needed, improving performance for large proofs.
5
6use crate::proof::{Proof, ProofNode, ProofNodeId, ProofStep};
7use rustc_hash::FxHashMap;
8use std::cell::RefCell;
9use std::rc::Rc;
10
11/// A lazily-evaluated proof node.
12#[derive(Debug, Clone)]
13pub struct LazyNode {
14    /// Node ID
15    pub id: ProofNodeId,
16    /// Cached node data (None if not yet evaluated)
17    cached: Rc<RefCell<Option<ProofNode>>>,
18    /// Proof reference for lazy loading
19    proof: Rc<Proof>,
20}
21
22impl LazyNode {
23    /// Create a new lazy node.
24    fn new(id: ProofNodeId, proof: Rc<Proof>) -> Self {
25        Self {
26            id,
27            cached: Rc::new(RefCell::new(None)),
28            proof,
29        }
30    }
31
32    /// Force evaluation and get the node.
33    pub fn force(&self) -> Option<ProofNode> {
34        // Check cache first
35        if let Some(node) = self.cached.borrow().as_ref() {
36            return Some(node.clone());
37        }
38
39        // Load from proof
40        if let Some(node) = self.proof.get_node(self.id) {
41            *self.cached.borrow_mut() = Some(node.clone());
42            Some(node.clone())
43        } else {
44            None
45        }
46    }
47
48    /// Check if node has been evaluated.
49    pub fn is_forced(&self) -> bool {
50        self.cached.borrow().is_some()
51    }
52
53    /// Get the conclusion without full evaluation (if cached).
54    pub fn conclusion_if_cached(&self) -> Option<String> {
55        self.cached
56            .borrow()
57            .as_ref()
58            .map(|node| node.conclusion().to_string())
59    }
60}
61
62/// Lazy proof wrapper for deferred computation.
63pub struct LazyProof {
64    /// The underlying proof
65    proof: Rc<Proof>,
66    /// Lazy node cache
67    node_cache: RefCell<FxHashMap<ProofNodeId, LazyNode>>,
68    /// Statistics on cache hits/misses
69    stats: RefCell<LazyStats>,
70}
71
72/// Statistics for lazy evaluation.
73#[derive(Debug, Clone, Default)]
74pub struct LazyStats {
75    /// Number of cache hits
76    pub cache_hits: usize,
77    /// Number of cache misses
78    pub cache_misses: usize,
79    /// Number of forced evaluations
80    pub forced_evaluations: usize,
81}
82
83impl LazyStats {
84    /// Get the cache hit rate.
85    pub fn hit_rate(&self) -> f64 {
86        let total = self.cache_hits + self.cache_misses;
87        if total == 0 {
88            0.0
89        } else {
90            self.cache_hits as f64 / total as f64
91        }
92    }
93}
94
95impl LazyProof {
96    /// Create a new lazy proof wrapper.
97    pub fn new(proof: Proof) -> Self {
98        Self {
99            proof: Rc::new(proof),
100            node_cache: RefCell::new(FxHashMap::default()),
101            stats: RefCell::new(LazyStats::default()),
102        }
103    }
104
105    /// Get a lazy node by ID.
106    pub fn get_lazy_node(&self, id: ProofNodeId) -> Option<LazyNode> {
107        // Check cache
108        if let Some(lazy_node) = self.node_cache.borrow().get(&id) {
109            self.stats.borrow_mut().cache_hits += 1;
110            return Some(lazy_node.clone());
111        }
112
113        // Create new lazy node
114        self.stats.borrow_mut().cache_misses += 1;
115        if self.proof.get_node(id).is_some() {
116            let lazy_node = LazyNode::new(id, Rc::clone(&self.proof));
117            self.node_cache.borrow_mut().insert(id, lazy_node.clone());
118            Some(lazy_node)
119        } else {
120            None
121        }
122    }
123
124    /// Force evaluation of a node.
125    pub fn force_node(&self, id: ProofNodeId) -> Option<ProofNode> {
126        if let Some(lazy_node) = self.get_lazy_node(id) {
127            self.stats.borrow_mut().forced_evaluations += 1;
128            lazy_node.force()
129        } else {
130            None
131        }
132    }
133
134    /// Get premises of a node lazily.
135    pub fn get_premises_lazy(&self, id: ProofNodeId) -> Vec<LazyNode> {
136        if let Some(node) = self.force_node(id) {
137            if let ProofStep::Inference { premises, .. } = &node.step {
138                premises
139                    .iter()
140                    .filter_map(|&p| self.get_lazy_node(p))
141                    .collect()
142            } else {
143                Vec::new()
144            }
145        } else {
146            Vec::new()
147        }
148    }
149
150    /// Check if a node is an axiom without forcing full evaluation.
151    pub fn is_axiom_lazy(&self, id: ProofNodeId) -> bool {
152        if let Some(node) = self.force_node(id) {
153            matches!(node.step, ProofStep::Axiom { .. })
154        } else {
155            false
156        }
157    }
158
159    /// Get the number of nodes in the proof.
160    pub fn len(&self) -> usize {
161        self.proof.len()
162    }
163
164    /// Check if the proof is empty.
165    pub fn is_empty(&self) -> bool {
166        self.proof.is_empty()
167    }
168
169    /// Get lazy evaluation statistics.
170    pub fn get_stats(&self) -> LazyStats {
171        self.stats.borrow().clone()
172    }
173
174    /// Clear the node cache.
175    pub fn clear_cache(&self) {
176        self.node_cache.borrow_mut().clear();
177    }
178
179    /// Get the underlying proof reference.
180    pub fn proof(&self) -> &Proof {
181        &self.proof
182    }
183}
184
185/// Lazy iterator over proof nodes.
186pub struct LazyNodeIterator {
187    lazy_proof: Rc<LazyProof>,
188    current_index: usize,
189    node_ids: Vec<ProofNodeId>,
190}
191
192impl LazyNodeIterator {
193    /// Create a new lazy iterator.
194    pub fn new(lazy_proof: &LazyProof) -> Self {
195        let node_ids = lazy_proof.proof.nodes().iter().map(|n| n.id).collect();
196        Self {
197            lazy_proof: Rc::new(LazyProof::new((*lazy_proof.proof).clone())),
198            current_index: 0,
199            node_ids,
200        }
201    }
202}
203
204impl Iterator for LazyNodeIterator {
205    type Item = LazyNode;
206
207    fn next(&mut self) -> Option<Self::Item> {
208        if self.current_index < self.node_ids.len() {
209            let id = self.node_ids[self.current_index];
210            self.current_index += 1;
211            self.lazy_proof.get_lazy_node(id)
212        } else {
213            None
214        }
215    }
216}
217
218/// Lazy dependency resolver.
219pub struct LazyDependencyResolver {
220    lazy_proof: LazyProof,
221}
222
223impl LazyDependencyResolver {
224    /// Create a new lazy dependency resolver.
225    pub fn new(proof: Proof) -> Self {
226        Self {
227            lazy_proof: LazyProof::new(proof),
228        }
229    }
230
231    /// Get all dependencies of a node lazily.
232    pub fn get_dependencies(&self, id: ProofNodeId) -> Vec<ProofNodeId> {
233        let mut dependencies = Vec::new();
234        let mut visited = FxHashMap::default();
235        self.collect_dependencies(id, &mut dependencies, &mut visited);
236        dependencies
237    }
238
239    // Helper: Recursively collect dependencies
240    fn collect_dependencies(
241        &self,
242        id: ProofNodeId,
243        deps: &mut Vec<ProofNodeId>,
244        visited: &mut FxHashMap<ProofNodeId, bool>,
245    ) {
246        if visited.contains_key(&id) {
247            return;
248        }
249        visited.insert(id, true);
250
251        let premises = self.lazy_proof.get_premises_lazy(id);
252        for premise in premises {
253            self.collect_dependencies(premise.id, deps, visited);
254        }
255
256        deps.push(id);
257    }
258
259    /// Check if node A depends on node B.
260    pub fn depends_on(&self, a: ProofNodeId, b: ProofNodeId) -> bool {
261        self.get_dependencies(a).contains(&b)
262    }
263
264    /// Get evaluation statistics.
265    pub fn get_stats(&self) -> LazyStats {
266        self.lazy_proof.get_stats()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_lazy_node_new() {
276        let proof = Proof::new();
277        let proof_rc = Rc::new(proof);
278        let lazy_node = LazyNode::new(ProofNodeId(0), proof_rc);
279        assert_eq!(lazy_node.id, ProofNodeId(0));
280        assert!(!lazy_node.is_forced());
281    }
282
283    #[test]
284    fn test_lazy_proof_new() {
285        let proof = Proof::new();
286        let lazy_proof = LazyProof::new(proof);
287        assert!(lazy_proof.is_empty());
288    }
289
290    #[test]
291    fn test_get_lazy_node() {
292        let mut proof = Proof::new();
293        let id = proof.add_axiom("x = x");
294        let lazy_proof = LazyProof::new(proof);
295
296        let lazy_node = lazy_proof.get_lazy_node(id);
297        assert!(lazy_node.is_some());
298    }
299
300    #[test]
301    fn test_force_node() {
302        let mut proof = Proof::new();
303        let id = proof.add_axiom("x = x");
304        let lazy_proof = LazyProof::new(proof);
305
306        let node = lazy_proof.force_node(id);
307        assert!(node.is_some());
308        assert_eq!(
309            node.expect("test operation should succeed").conclusion(),
310            "x = x"
311        );
312    }
313
314    #[test]
315    fn test_is_axiom_lazy() {
316        let mut proof = Proof::new();
317        let id = proof.add_axiom("x = x");
318        let lazy_proof = LazyProof::new(proof);
319
320        assert!(lazy_proof.is_axiom_lazy(id));
321    }
322
323    #[test]
324    fn test_lazy_stats() {
325        let mut proof = Proof::new();
326        let id = proof.add_axiom("x = x");
327        let lazy_proof = LazyProof::new(proof);
328
329        // First access - cache miss
330        let _ = lazy_proof.get_lazy_node(id);
331        let stats = lazy_proof.get_stats();
332        assert_eq!(stats.cache_misses, 1);
333
334        // Second access - cache hit
335        let _ = lazy_proof.get_lazy_node(id);
336        let stats = lazy_proof.get_stats();
337        assert_eq!(stats.cache_hits, 1);
338    }
339
340    #[test]
341    fn test_lazy_stats_hit_rate() {
342        let stats = LazyStats {
343            cache_hits: 7,
344            cache_misses: 3,
345            forced_evaluations: 10,
346        };
347        assert_eq!(stats.hit_rate(), 0.7);
348    }
349
350    #[test]
351    fn test_clear_cache() {
352        let mut proof = Proof::new();
353        let id = proof.add_axiom("x = x");
354        let lazy_proof = LazyProof::new(proof);
355
356        let _ = lazy_proof.get_lazy_node(id);
357        lazy_proof.clear_cache();
358
359        // Should be cache miss after clear
360        let _ = lazy_proof.get_lazy_node(id);
361        let stats = lazy_proof.get_stats();
362        assert_eq!(stats.cache_misses, 2); // One before clear, one after
363    }
364
365    #[test]
366    fn test_lazy_dependency_resolver() {
367        let mut proof = Proof::new();
368        let ax1 = proof.add_axiom("x = x");
369        let ax2 = proof.add_axiom("y = y");
370        let res = proof.add_inference("resolution", vec![ax1, ax2], "x = x or y = y");
371
372        let resolver = LazyDependencyResolver::new(proof);
373        let deps = resolver.get_dependencies(res);
374        assert_eq!(deps.len(), 3); // ax1, ax2, res
375    }
376
377    #[test]
378    fn test_depends_on() {
379        let mut proof = Proof::new();
380        let ax1 = proof.add_axiom("x = x");
381        let ax2 = proof.add_axiom("y = y");
382        let res = proof.add_inference("resolution", vec![ax1, ax2], "x = x or y = y");
383
384        let resolver = LazyDependencyResolver::new(proof);
385        assert!(resolver.depends_on(res, ax1));
386        assert!(resolver.depends_on(res, ax2));
387        assert!(!resolver.depends_on(ax1, res));
388    }
389
390    #[test]
391    fn test_get_premises_lazy() {
392        let mut proof = Proof::new();
393        let ax1 = proof.add_axiom("x = x");
394        let ax2 = proof.add_axiom("y = y");
395        let res = proof.add_inference("resolution", vec![ax1, ax2], "x = x or y = y");
396
397        let lazy_proof = LazyProof::new(proof);
398        let premises = lazy_proof.get_premises_lazy(res);
399        assert_eq!(premises.len(), 2);
400    }
401}