Skip to main content

oxiz_proof/
simplify.rs

1//! Proof simplification through logical rewriting.
2//!
3//! This module provides simplification of proof steps by applying logical
4//! rewrite rules, combining redundant inferences, and normalizing conclusions.
5//! Unlike compression (which removes unreachable nodes), simplification
6//! transforms proof steps to equivalent but simpler forms.
7
8use crate::proof::Proof;
9
10/// Configuration for proof simplification.
11#[derive(Debug, Clone)]
12pub struct SimplificationConfig {
13    /// Apply De Morgan's laws to normalize negations
14    pub apply_demorgan: bool,
15    /// Simplify double negations (¬¬p → p)
16    pub simplify_double_negation: bool,
17    /// Remove identity operations (p ∧ true → p)
18    pub remove_identities: bool,
19    /// Combine consecutive inferences when possible
20    pub combine_inferences: bool,
21    /// Simplify tautologies (p ∨ ¬p → true)
22    pub simplify_tautologies: bool,
23    /// Maximum number of simplification passes
24    pub max_passes: usize,
25}
26
27impl Default for SimplificationConfig {
28    fn default() -> Self {
29        Self {
30            apply_demorgan: true,
31            simplify_double_negation: true,
32            remove_identities: true,
33            combine_inferences: true,
34            simplify_tautologies: true,
35            max_passes: 5,
36        }
37    }
38}
39
40impl SimplificationConfig {
41    /// Create a new configuration with all simplifications enabled.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Disable De Morgan's law simplification.
47    pub fn without_demorgan(mut self) -> Self {
48        self.apply_demorgan = false;
49        self
50    }
51
52    /// Set maximum number of passes.
53    pub fn with_max_passes(mut self, passes: usize) -> Self {
54        self.max_passes = passes;
55        self
56    }
57}
58
59/// Statistics about simplification operations.
60#[derive(Debug, Clone, Default)]
61pub struct SimplificationStats {
62    /// Number of simplification passes performed
63    pub passes: usize,
64    /// Number of double negations simplified
65    pub double_negations: usize,
66    /// Number of De Morgan transformations applied
67    pub demorgan_applications: usize,
68    /// Number of identity operations removed
69    pub identities_removed: usize,
70    /// Number of inferences combined
71    pub inferences_combined: usize,
72    /// Number of tautologies simplified
73    pub tautologies_simplified: usize,
74    /// Total nodes before simplification
75    pub nodes_before: usize,
76    /// Total nodes after simplification
77    pub nodes_after: usize,
78}
79
80impl SimplificationStats {
81    /// Calculate total simplifications applied.
82    pub fn total_simplifications(&self) -> usize {
83        self.double_negations
84            + self.demorgan_applications
85            + self.identities_removed
86            + self.inferences_combined
87            + self.tautologies_simplified
88    }
89
90    /// Calculate reduction ratio (0.0 = no reduction, 1.0 = complete reduction).
91    pub fn reduction_ratio(&self) -> f64 {
92        if self.nodes_before == 0 {
93            0.0
94        } else {
95            1.0 - (self.nodes_after as f64 / self.nodes_before as f64)
96        }
97    }
98}
99
100/// Proof simplifier that applies logical rewrite rules.
101pub struct ProofSimplifier {
102    config: SimplificationConfig,
103}
104
105impl ProofSimplifier {
106    /// Create a new simplifier with default configuration.
107    pub fn new() -> Self {
108        Self {
109            config: SimplificationConfig::default(),
110        }
111    }
112
113    /// Create a simplifier with custom configuration.
114    pub fn with_config(config: SimplificationConfig) -> Self {
115        Self { config }
116    }
117
118    /// Simplify a proof in-place, returning statistics.
119    pub fn simplify(&self, proof: &mut Proof) -> SimplificationStats {
120        let mut stats = SimplificationStats {
121            nodes_before: proof.node_count(),
122            ..Default::default()
123        };
124
125        for pass in 0..self.config.max_passes {
126            let mut changed = false;
127
128            if self.config.simplify_double_negation {
129                let count = self.simplify_double_negations(proof);
130                stats.double_negations += count;
131                changed |= count > 0;
132            }
133
134            if self.config.apply_demorgan {
135                let count = self.apply_demorgan_laws(proof);
136                stats.demorgan_applications += count;
137                changed |= count > 0;
138            }
139
140            if self.config.remove_identities {
141                let count = self.remove_identity_operations(proof);
142                stats.identities_removed += count;
143                changed |= count > 0;
144            }
145
146            if self.config.simplify_tautologies {
147                let count = self.simplify_tautology_steps(proof);
148                stats.tautologies_simplified += count;
149                changed |= count > 0;
150            }
151
152            if self.config.combine_inferences {
153                let count = self.combine_inference_chains(proof);
154                stats.inferences_combined += count;
155                changed |= count > 0;
156            }
157
158            stats.passes = pass + 1;
159
160            if !changed {
161                break;
162            }
163        }
164
165        stats.nodes_after = proof.node_count();
166        stats
167    }
168
169    /// Simplify double negations (¬¬p → p).
170    fn simplify_double_negations(&self, proof: &mut Proof) -> usize {
171        let mut count = 0;
172        let node_ids: Vec<_> = proof.nodes().iter().map(|node| node.id).collect();
173
174        for id in node_ids {
175            if let Some(node) = proof.get_node(id) {
176                let conclusion = node.conclusion().to_string();
177                if let Some(simplified) = self.simplify_conclusion_double_neg(&conclusion)
178                    && simplified != conclusion
179                {
180                    // Update the conclusion
181                    if proof.update_conclusion(id, simplified) {
182                        count += 1;
183                    }
184                }
185            }
186        }
187
188        count
189    }
190
191    /// Apply De Morgan's laws (¬(p ∧ q) → ¬p ∨ ¬q).
192    fn apply_demorgan_laws(&self, proof: &mut Proof) -> usize {
193        let mut count = 0;
194        let node_ids: Vec<_> = proof.nodes().iter().map(|node| node.id).collect();
195
196        for id in node_ids {
197            if let Some(node) = proof.get_node(id) {
198                let conclusion = node.conclusion().to_string();
199                if let Some(simplified) = self.apply_demorgan_to_conclusion(&conclusion)
200                    && simplified != conclusion
201                    && proof.update_conclusion(id, simplified)
202                {
203                    count += 1;
204                }
205            }
206        }
207
208        count
209    }
210
211    /// Remove identity operations (p ∧ true → p, p ∨ false → p).
212    fn remove_identity_operations(&self, proof: &mut Proof) -> usize {
213        let mut count = 0;
214        let node_ids: Vec<_> = proof.nodes().iter().map(|node| node.id).collect();
215
216        for id in node_ids {
217            if let Some(node) = proof.get_node(id) {
218                let conclusion = node.conclusion().to_string();
219                if let Some(simplified) = self.remove_identities_from_conclusion(&conclusion)
220                    && simplified != conclusion
221                    && proof.update_conclusion(id, simplified)
222                {
223                    count += 1;
224                }
225            }
226        }
227
228        count
229    }
230
231    /// Simplify tautological steps (p ∨ ¬p → true).
232    fn simplify_tautology_steps(&self, proof: &mut Proof) -> usize {
233        let mut count = 0;
234        let node_ids: Vec<_> = proof.nodes().iter().map(|node| node.id).collect();
235
236        for id in node_ids {
237            if let Some(node) = proof.get_node(id) {
238                let conclusion = node.conclusion().to_string();
239                if self.is_tautology(&conclusion) && proof.update_conclusion(id, "true") {
240                    count += 1;
241                }
242            }
243        }
244
245        count
246    }
247
248    /// Combine consecutive inference chains when possible.
249    fn combine_inference_chains(&self, _proof: &mut Proof) -> usize {
250        // This is more complex and requires analyzing inference patterns
251        // For now, return 0 (future enhancement)
252        0
253    }
254
255    // Helper methods for conclusion simplification
256
257    fn simplify_conclusion_double_neg(&self, conclusion: &str) -> Option<String> {
258        let s = conclusion.trim();
259
260        // Match patterns like: (not (not p))
261        if s.starts_with("(not (not ") && s.ends_with("))") {
262            let inner = &s[10..s.len() - 2];
263            return Some(inner.trim().to_string());
264        }
265
266        // Match patterns like: ¬¬p (UTF-8 safe)
267        if s.starts_with("¬") {
268            let chars: Vec<char> = s.chars().collect();
269            if chars.len() >= 3 && chars[0] == '¬' && chars[1] == '¬' {
270                let result: String = chars[2..].iter().collect();
271                return Some(result.trim().to_string());
272            }
273        }
274
275        None
276    }
277
278    fn apply_demorgan_to_conclusion(&self, conclusion: &str) -> Option<String> {
279        let s = conclusion.trim();
280
281        // Match: (not (and p q)) → (or (not p) (not q))
282        if s.starts_with("(not (and ") && s.ends_with("))") {
283            let inner_content = &s[10..s.len() - 2];
284            if let Some(inner) = self.extract_binary_args(&format!("{})", inner_content)) {
285                return Some(format!("(or (not {}) (not {}))", inner.0, inner.1));
286            }
287        }
288
289        // Match: (not (or p q)) → (and (not p) (not q))
290        if s.starts_with("(not (or ") && s.ends_with("))") {
291            let inner_content = &s[9..s.len() - 2];
292            if let Some(inner) = self.extract_binary_args(&format!("{})", inner_content)) {
293                return Some(format!("(and (not {}) (not {}))", inner.0, inner.1));
294            }
295        }
296
297        None
298    }
299
300    fn remove_identities_from_conclusion(&self, conclusion: &str) -> Option<String> {
301        let s = conclusion.trim();
302
303        // Match: (and p true) → p
304        if (s.contains(" true)") || s.contains(" true ))"))
305            && s.starts_with("(and ")
306            && let Some((left, right)) = self.extract_binary_args(&s[5..])
307        {
308            if right.trim() == "true" {
309                return Some(left.to_string());
310            }
311            if left.trim() == "true" {
312                return Some(right.to_string());
313            }
314        }
315
316        // Match: (or p false) → p
317        if (s.contains(" false)") || s.contains(" false ))"))
318            && s.starts_with("(or ")
319            && let Some((left, right)) = self.extract_binary_args(&s[4..])
320        {
321            if right.trim() == "false" {
322                return Some(left.to_string());
323            }
324            if left.trim() == "false" {
325                return Some(right.to_string());
326            }
327        }
328
329        None
330    }
331
332    fn is_tautology(&self, conclusion: &str) -> bool {
333        let s = conclusion.trim();
334
335        // Check for patterns like: (or p (not p))
336        if let Some(stripped) = s.strip_prefix("(or ")
337            && let Some((left, right)) = self.extract_binary_args(stripped)
338        {
339            // Check if right is (not left) or left is (not right)
340            if right.trim() == format!("(not {})", left.trim()) {
341                return true;
342            }
343            if left.trim() == format!("(not {})", right.trim()) {
344                return true;
345            }
346        }
347
348        false
349    }
350
351    fn extract_binary_args(&self, s: &str) -> Option<(String, String)> {
352        // Simple parser for binary operations
353        // This is a basic implementation; a full s-expression parser would be better
354        let trimmed = s.trim();
355        if !trimmed.ends_with(')') {
356            return None;
357        }
358
359        let content = &trimmed[..trimmed.len() - 1];
360        let mut depth = 0;
361        let mut split_pos = None;
362
363        for (i, ch) in content.chars().enumerate() {
364            match ch {
365                '(' => depth += 1,
366                ')' => depth -= 1,
367                ' ' if depth == 0 => {
368                    split_pos = Some(i);
369                    break;
370                }
371                _ => {}
372            }
373        }
374
375        if let Some(pos) = split_pos {
376            let left = content[..pos].trim().to_string();
377            let right = content[pos + 1..].trim().to_string();
378            Some((left, right))
379        } else {
380            None
381        }
382    }
383}
384
385impl Default for ProofSimplifier {
386    fn default() -> Self {
387        Self::new()
388    }
389}
390
391/// Simplify a proof using default configuration.
392pub fn simplify_proof(proof: &mut Proof) -> SimplificationStats {
393    ProofSimplifier::new().simplify(proof)
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::proof::ProofNodeId;
400
401    #[test]
402    fn test_simplification_config_default() {
403        let config = SimplificationConfig::default();
404        assert!(config.apply_demorgan);
405        assert!(config.simplify_double_negation);
406        assert_eq!(config.max_passes, 5);
407    }
408
409    #[test]
410    fn test_simplification_config_builder() {
411        let config = SimplificationConfig::new()
412            .without_demorgan()
413            .with_max_passes(3);
414        assert!(!config.apply_demorgan);
415        assert_eq!(config.max_passes, 3);
416    }
417
418    #[test]
419    fn test_simplification_stats_total() {
420        let stats = SimplificationStats {
421            double_negations: 2,
422            demorgan_applications: 3,
423            identities_removed: 1,
424            ..Default::default()
425        };
426        assert_eq!(stats.total_simplifications(), 6);
427    }
428
429    #[test]
430    fn test_simplification_stats_reduction_ratio() {
431        let stats = SimplificationStats {
432            nodes_before: 100,
433            nodes_after: 80,
434            ..Default::default()
435        };
436        assert!((stats.reduction_ratio() - 0.2).abs() < 1e-6);
437    }
438
439    #[test]
440    fn test_simplify_double_negation_sexp() {
441        let simplifier = ProofSimplifier::new();
442        let conclusion = "(not (not p))";
443        let result = simplifier.simplify_conclusion_double_neg(conclusion);
444        assert_eq!(result, Some("p".to_string()));
445    }
446
447    #[test]
448    fn test_simplify_double_negation_unicode() {
449        let simplifier = ProofSimplifier::new();
450        let conclusion = "¬¬p";
451        let result = simplifier.simplify_conclusion_double_neg(conclusion);
452        assert_eq!(result, Some("p".to_string()));
453    }
454
455    #[test]
456    fn test_apply_demorgan_not_and() {
457        let simplifier = ProofSimplifier::new();
458        let conclusion = "(not (and p q))";
459        let result = simplifier.apply_demorgan_to_conclusion(conclusion);
460        assert_eq!(result, Some("(or (not p) (not q))".to_string()));
461    }
462
463    #[test]
464    fn test_apply_demorgan_not_or() {
465        let simplifier = ProofSimplifier::new();
466        let conclusion = "(not (or p q))";
467        let result = simplifier.apply_demorgan_to_conclusion(conclusion);
468        assert_eq!(result, Some("(and (not p) (not q))".to_string()));
469    }
470
471    #[test]
472    fn test_remove_identity_and_true() {
473        let simplifier = ProofSimplifier::new();
474        let conclusion = "(and p true)";
475        let result = simplifier.remove_identities_from_conclusion(conclusion);
476        assert_eq!(result, Some("p".to_string()));
477    }
478
479    #[test]
480    fn test_remove_identity_or_false() {
481        let simplifier = ProofSimplifier::new();
482        let conclusion = "(or p false)";
483        let result = simplifier.remove_identities_from_conclusion(conclusion);
484        assert_eq!(result, Some("p".to_string()));
485    }
486
487    #[test]
488    fn test_is_tautology_or_not() {
489        let simplifier = ProofSimplifier::new();
490        assert!(simplifier.is_tautology("(or p (not p))"));
491        assert!(simplifier.is_tautology("(or (not p) p)"));
492        assert!(!simplifier.is_tautology("(or p q)"));
493    }
494
495    #[test]
496    fn test_extract_binary_args_simple() {
497        let simplifier = ProofSimplifier::new();
498        let result = simplifier.extract_binary_args("p q)");
499        assert_eq!(result, Some(("p".to_string(), "q".to_string())));
500    }
501
502    #[test]
503    fn test_extract_binary_args_nested() {
504        let simplifier = ProofSimplifier::new();
505        let result = simplifier.extract_binary_args("(and a b) q)");
506        assert_eq!(result, Some(("(and a b)".to_string(), "q".to_string())));
507    }
508
509    #[test]
510    fn test_simplify_proof_empty() {
511        let mut proof = Proof::new();
512        let stats = simplify_proof(&mut proof);
513        assert_eq!(stats.total_simplifications(), 0);
514        assert_eq!(stats.nodes_before, 0);
515        assert_eq!(stats.nodes_after, 0);
516    }
517
518    #[test]
519    fn test_simplify_proof_double_negation() {
520        let mut proof = Proof::new();
521        proof.add_axiom("(not (not p))".to_string());
522
523        let stats = simplify_proof(&mut proof);
524        assert!(stats.double_negations > 0);
525
526        // Check that conclusion was simplified
527        let node = proof
528            .get_node(ProofNodeId(0))
529            .expect("test operation should succeed");
530        assert_eq!(node.conclusion(), "p");
531    }
532
533    #[test]
534    fn test_simplify_proof_identity() {
535        let mut proof = Proof::new();
536        proof.add_axiom("(and p true)".to_string());
537
538        let stats = simplify_proof(&mut proof);
539        assert!(stats.identities_removed > 0);
540
541        let node = proof
542            .get_node(ProofNodeId(0))
543            .expect("test operation should succeed");
544        assert_eq!(node.conclusion(), "p");
545    }
546
547    #[test]
548    fn test_simplify_proof_tautology() {
549        let mut proof = Proof::new();
550        proof.add_axiom("(or p (not p))".to_string());
551
552        let stats = simplify_proof(&mut proof);
553        assert!(stats.tautologies_simplified > 0);
554
555        let node = proof
556            .get_node(ProofNodeId(0))
557            .expect("test operation should succeed");
558        assert_eq!(node.conclusion(), "true");
559    }
560
561    #[test]
562    fn test_simplify_proof_multiple_passes() {
563        let mut proof = Proof::new();
564        // Double negation of identity: (not (not (and p true)))
565        // Should simplify in multiple passes: → (and p true) → p
566        proof.add_axiom("(not (not (and p true)))".to_string());
567
568        let config = SimplificationConfig::new().with_max_passes(3);
569        let simplifier = ProofSimplifier::with_config(config);
570        let stats = simplifier.simplify(&mut proof);
571
572        assert!(stats.passes >= 2);
573        // Note: Current implementation may not fully simplify this in all cases
574        // This test validates that multiple passes occur
575    }
576
577    #[test]
578    fn test_simplifier_with_custom_config() {
579        let config = SimplificationConfig::new().without_demorgan();
580        let simplifier = ProofSimplifier::with_config(config);
581
582        let mut proof = Proof::new();
583        proof.add_axiom("(not (and p q))".to_string());
584
585        let stats = simplifier.simplify(&mut proof);
586        // De Morgan should not be applied
587        assert_eq!(stats.demorgan_applications, 0);
588    }
589}