Skip to main content

oxiz_proof/
normalize.rs

1//! Proof normalization utilities for canonical representation.
2
3use crate::proof::{Proof, ProofNodeId, ProofStep};
4use rustc_hash::FxHashMap;
5
6/// Normalize a proof by reordering axioms alphabetically and renumbering nodes.
7///
8/// This creates a canonical representation of the proof that is independent of
9/// the order in which axioms and inferences were added.
10///
11/// # Arguments
12///
13/// * `proof` - The proof to normalize
14///
15/// # Returns
16///
17/// A normalized proof with axioms in alphabetical order
18///
19/// # Example
20///
21/// ```
22/// use oxiz_proof::proof::Proof;
23/// use oxiz_proof::normalize::normalize_proof;
24///
25/// let mut proof = Proof::new();
26/// proof.add_axiom("q");
27/// proof.add_axiom("p");
28/// proof.add_axiom("r");
29///
30/// let normalized = normalize_proof(&proof);
31/// // Axioms will be in order: p, q, r
32/// ```
33pub fn normalize_proof(proof: &Proof) -> Proof {
34    let mut normalized = Proof::new();
35    let mut id_map: FxHashMap<ProofNodeId, ProofNodeId> = FxHashMap::default();
36
37    // Collect and sort axioms
38    let mut axioms: Vec<(ProofNodeId, String)> = Vec::new();
39    let mut inferences: Vec<(ProofNodeId, &ProofStep)> = Vec::new();
40
41    for node in proof.nodes() {
42        match &node.step {
43            ProofStep::Axiom { conclusion } => {
44                axioms.push((node.id, conclusion.clone()));
45            }
46            ProofStep::Inference { .. } => {
47                inferences.push((node.id, &node.step));
48            }
49        }
50    }
51
52    // Sort axioms alphabetically by conclusion
53    axioms.sort_by(|a, b| a.1.cmp(&b.1));
54
55    // Add sorted axioms to normalized proof
56    for (old_id, conclusion) in axioms {
57        let new_id = normalized.add_axiom(conclusion);
58        id_map.insert(old_id, new_id);
59    }
60
61    // Process inferences in topological order
62    let mut added = std::collections::HashSet::new();
63    let mut changed = true;
64
65    while changed && added.len() < inferences.len() {
66        changed = false;
67
68        for &(old_id, step) in &inferences {
69            if added.contains(&old_id) {
70                continue;
71            }
72
73            if let ProofStep::Inference {
74                rule,
75                premises,
76                conclusion,
77                args,
78            } = step
79            {
80                // Check if all premises have been added
81                let all_premises_ready = premises.iter().all(|&p| id_map.contains_key(&p));
82
83                if all_premises_ready {
84                    let new_premises: Vec<ProofNodeId> = premises
85                        .iter()
86                        .filter_map(|&old_p| id_map.get(&old_p).copied())
87                        .collect();
88
89                    let new_id = if args.is_empty() {
90                        normalized.add_inference(rule, new_premises, conclusion.clone())
91                    } else {
92                        normalized.add_inference_with_args(
93                            rule,
94                            new_premises,
95                            args.to_vec(),
96                            conclusion.clone(),
97                        )
98                    };
99
100                    id_map.insert(old_id, new_id);
101                    added.insert(old_id);
102                    changed = true;
103                }
104            }
105        }
106    }
107
108    normalized
109}
110
111/// Canonicalize conclusion strings by removing extra whitespace.
112///
113/// This standardizes conclusion formatting for easier comparison.
114///
115/// # Arguments
116///
117/// * `proof` - The proof to canonicalize
118///
119/// # Returns
120///
121/// A new proof with canonicalized conclusions
122pub fn canonicalize_conclusions(proof: &Proof) -> Proof {
123    let mut canonical = Proof::new();
124    let mut id_map: FxHashMap<ProofNodeId, ProofNodeId> = FxHashMap::default();
125
126    for node in proof.nodes() {
127        let new_id = match &node.step {
128            ProofStep::Axiom { conclusion } => {
129                let canonical_conclusion = canonicalize_string(conclusion);
130                canonical.add_axiom(canonical_conclusion)
131            }
132            ProofStep::Inference {
133                rule,
134                premises,
135                conclusion,
136                args,
137            } => {
138                let canonical_conclusion = canonicalize_string(conclusion);
139                let new_premises: Vec<ProofNodeId> = premises
140                    .iter()
141                    .filter_map(|&old_p| id_map.get(&old_p).copied())
142                    .collect();
143
144                if args.is_empty() {
145                    canonical.add_inference(rule, new_premises, canonical_conclusion)
146                } else {
147                    canonical.add_inference_with_args(
148                        rule,
149                        new_premises,
150                        args.to_vec(),
151                        canonical_conclusion,
152                    )
153                }
154            }
155        };
156
157        id_map.insert(node.id, new_id);
158    }
159
160    canonical
161}
162
163/// Canonicalize a string by removing extra whitespace and trimming.
164fn canonicalize_string(s: &str) -> String {
165    s.split_whitespace().collect::<Vec<_>>().join(" ")
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_normalize_proof_axioms() {
174        let mut proof = Proof::new();
175        proof.add_axiom("z");
176        proof.add_axiom("a");
177        proof.add_axiom("m");
178
179        let normalized = normalize_proof(&proof);
180
181        // Check that axioms are sorted
182        let nodes: Vec<_> = normalized.nodes().iter().collect();
183        assert_eq!(nodes.len(), 3);
184
185        if let ProofStep::Axiom { conclusion } = &nodes[0].step {
186            assert_eq!(conclusion, "a");
187        } else {
188            panic!("Expected axiom");
189        }
190
191        if let ProofStep::Axiom { conclusion } = &nodes[1].step {
192            assert_eq!(conclusion, "m");
193        } else {
194            panic!("Expected axiom");
195        }
196
197        if let ProofStep::Axiom { conclusion } = &nodes[2].step {
198            assert_eq!(conclusion, "z");
199        } else {
200            panic!("Expected axiom");
201        }
202    }
203
204    #[test]
205    fn test_normalize_proof_with_inferences() {
206        let mut proof = Proof::new();
207        let z = proof.add_axiom("z");
208        let a = proof.add_axiom("a");
209        proof.add_inference("and", vec![z, a], "z ∧ a");
210
211        let normalized = normalize_proof(&proof);
212
213        assert_eq!(normalized.len(), 3);
214    }
215
216    #[test]
217    fn test_canonicalize_conclusions() {
218        let mut proof = Proof::new();
219        proof.add_axiom("p   q   r"); // Extra spaces
220        proof.add_axiom("  x  y  "); // Leading/trailing spaces
221
222        let canonical = canonicalize_conclusions(&proof);
223
224        let nodes: Vec<_> = canonical.nodes().iter().collect();
225        if let ProofStep::Axiom { conclusion } = &nodes[0].step {
226            assert_eq!(conclusion, "p q r");
227        } else {
228            panic!("Expected axiom");
229        }
230
231        if let ProofStep::Axiom { conclusion } = &nodes[1].step {
232            assert_eq!(conclusion, "x y");
233        } else {
234            panic!("Expected axiom");
235        }
236    }
237
238    #[test]
239    fn test_canonicalize_string() {
240        assert_eq!(canonicalize_string("  a  b  c  "), "a b c");
241        assert_eq!(canonicalize_string("x"), "x");
242        assert_eq!(canonicalize_string("  "), "");
243    }
244
245    #[test]
246    fn test_normalize_empty_proof() {
247        let proof = Proof::new();
248        let normalized = normalize_proof(&proof);
249        assert!(normalized.is_empty());
250    }
251
252    #[test]
253    fn test_canonicalize_empty_proof() {
254        let proof = Proof::new();
255        let canonical = canonicalize_conclusions(&proof);
256        assert!(canonical.is_empty());
257    }
258}