1use crate::proof::{Proof, ProofNodeId, ProofStep};
11use rustc_hash::FxHashSet;
12use std::collections::VecDeque;
13
14#[derive(Debug, Clone)]
16pub struct CompressionConfig {
17 pub remove_redundant: bool,
19 pub trim_unused: bool,
21 pub inline_trivial: bool,
23 pub max_passes: usize,
25}
26
27impl Default for CompressionConfig {
28 fn default() -> Self {
29 Self {
30 remove_redundant: true,
31 trim_unused: true,
32 inline_trivial: true,
33 max_passes: 3,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct CompressionResult {
41 pub nodes_before: usize,
43 pub nodes_after: usize,
45 pub nodes_removed: usize,
47 pub ratio: f64,
49}
50
51impl CompressionResult {
52 #[must_use]
54 pub fn new(nodes_before: usize, nodes_after: usize) -> Self {
55 let nodes_removed = nodes_before.saturating_sub(nodes_after);
56 let ratio = if nodes_before > 0 {
57 nodes_after as f64 / nodes_before as f64
58 } else {
59 1.0
60 };
61
62 Self {
63 nodes_before,
64 nodes_after,
65 nodes_removed,
66 ratio,
67 }
68 }
69}
70
71pub struct ProofCompressor {
73 config: CompressionConfig,
75}
76
77impl ProofCompressor {
78 #[must_use]
80 pub fn new() -> Self {
81 Self {
82 config: CompressionConfig::default(),
83 }
84 }
85
86 #[must_use]
88 pub fn with_config(config: CompressionConfig) -> Self {
89 Self { config }
90 }
91
92 pub fn compress(&self, proof: &Proof) -> (Proof, CompressionResult) {
94 let nodes_before = proof.len();
95 let mut compressed = proof.clone();
96
97 for _ in 0..self.config.max_passes {
98 let prev_len = compressed.len();
99
100 if self.config.trim_unused {
101 compressed = self.trim_unused_nodes(&compressed);
102 }
103
104 if self.config.remove_redundant {
105 compressed = self.remove_redundant(&compressed);
106 }
107
108 if self.config.inline_trivial {
109 compressed = self.inline_trivial_steps(&compressed);
110 }
111
112 if compressed.len() == prev_len {
114 break;
115 }
116 }
117
118 let nodes_after = compressed.len();
119 let result = CompressionResult::new(nodes_before, nodes_after);
120
121 (compressed, result)
122 }
123
124 fn trim_unused_nodes(&self, proof: &Proof) -> Proof {
126 let mut reachable = FxHashSet::default();
128 let mut queue = VecDeque::new();
129
130 for &root_id in proof.roots() {
132 queue.push_back(root_id);
133 reachable.insert(root_id);
134 }
135
136 while let Some(node_id) = queue.pop_front() {
138 if let Some(node) = proof.get_node(node_id)
139 && let ProofStep::Inference { premises, .. } = &node.step
140 {
141 for &premise_id in premises {
142 if reachable.insert(premise_id) {
143 queue.push_back(premise_id);
144 }
145 }
146 }
147 }
148
149 let mut new_proof = Proof::new();
151 let mut id_map = rustc_hash::FxHashMap::default();
152
153 for node in proof.nodes() {
155 if reachable.contains(&node.id) {
156 let new_id = match &node.step {
157 ProofStep::Axiom { conclusion } => new_proof.add_axiom(conclusion.clone()),
158 ProofStep::Inference {
159 rule,
160 premises,
161 conclusion,
162 args,
163 } => {
164 let new_premises: Vec<ProofNodeId> = premises
165 .iter()
166 .filter_map(|p| id_map.get(p).copied())
167 .collect();
168
169 new_proof.add_inference_with_args(
170 rule.clone(),
171 new_premises,
172 args.to_vec(),
173 conclusion.clone(),
174 )
175 }
176 };
177 id_map.insert(node.id, new_id);
178 }
179 }
180
181 new_proof
182 }
183
184 fn remove_redundant(&self, proof: &Proof) -> Proof {
186 proof.clone()
188 }
189
190 fn inline_trivial_steps(&self, proof: &Proof) -> Proof {
192 let mut new_proof = Proof::new();
193 let mut id_map = rustc_hash::FxHashMap::default();
194
195 for node in proof.nodes() {
196 let is_trivial = match &node.step {
197 ProofStep::Axiom { .. } => false,
198 ProofStep::Inference { rule, premises, .. } => {
199 (rule == "refl" || rule == "identity") && premises.len() <= 1
201 }
202 };
203
204 if is_trivial {
205 if let ProofStep::Inference { premises, .. } = &node.step
207 && let Some(&premise_id) = premises.first()
208 && let Some(&mapped_premise) = id_map.get(&premise_id)
209 {
210 id_map.insert(node.id, mapped_premise);
211 continue;
212 }
213 }
214
215 let new_id = match &node.step {
217 ProofStep::Axiom { conclusion } => new_proof.add_axiom(conclusion.clone()),
218 ProofStep::Inference {
219 rule,
220 premises,
221 conclusion,
222 args,
223 } => {
224 let new_premises: Vec<ProofNodeId> = premises
225 .iter()
226 .filter_map(|p| id_map.get(p).copied())
227 .collect();
228
229 new_proof.add_inference_with_args(
230 rule.clone(),
231 new_premises,
232 args.to_vec(),
233 conclusion.clone(),
234 )
235 }
236 };
237 id_map.insert(node.id, new_id);
238 }
239
240 new_proof
241 }
242}
243
244impl Default for ProofCompressor {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250pub fn trim_to_conclusion(proof: &Proof, conclusion_id: ProofNodeId) -> Proof {
252 let mut needed = FxHashSet::default();
253 let mut queue = VecDeque::new();
254
255 queue.push_back(conclusion_id);
256 needed.insert(conclusion_id);
257
258 while let Some(node_id) = queue.pop_front() {
260 if let Some(node) = proof.get_node(node_id)
261 && let ProofStep::Inference { premises, .. } = &node.step
262 {
263 for &premise_id in premises {
264 if needed.insert(premise_id) {
265 queue.push_back(premise_id);
266 }
267 }
268 }
269 }
270
271 let mut new_proof = Proof::new();
273 let mut id_map = rustc_hash::FxHashMap::default();
274
275 for node in proof.nodes() {
276 if needed.contains(&node.id) {
277 let new_id = match &node.step {
278 ProofStep::Axiom { conclusion } => new_proof.add_axiom(conclusion.clone()),
279 ProofStep::Inference {
280 rule,
281 premises,
282 conclusion,
283 args,
284 } => {
285 let new_premises: Vec<ProofNodeId> = premises
286 .iter()
287 .filter_map(|p| id_map.get(p).copied())
288 .collect();
289
290 new_proof.add_inference_with_args(
291 rule.clone(),
292 new_premises,
293 args.to_vec(),
294 conclusion.clone(),
295 )
296 }
297 };
298 id_map.insert(node.id, new_id);
299 }
300 }
301
302 new_proof
303}
304
305#[must_use]
307pub fn get_dependency_cone(proof: &Proof, node_id: ProofNodeId) -> Vec<ProofNodeId> {
308 let mut deps = FxHashSet::default();
309 let mut queue = VecDeque::new();
310
311 queue.push_back(node_id);
312 deps.insert(node_id);
313
314 while let Some(id) = queue.pop_front() {
315 if let Some(node) = proof.get_node(id)
316 && let ProofStep::Inference { premises, .. } = &node.step
317 {
318 for &premise_id in premises {
319 if deps.insert(premise_id) {
320 queue.push_back(premise_id);
321 }
322 }
323 }
324 }
325
326 deps.into_iter().collect()
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_compression_result() {
335 let result = CompressionResult::new(100, 80);
336 assert_eq!(result.nodes_before, 100);
337 assert_eq!(result.nodes_after, 80);
338 assert_eq!(result.nodes_removed, 20);
339 assert!((result.ratio - 0.8).abs() < 0.01);
340 }
341
342 #[test]
343 fn test_trim_unused() {
344 let mut proof = Proof::new();
345 let a1 = proof.add_axiom("p");
346 let a2 = proof.add_axiom("q");
347 let _unused = proof.add_axiom("r"); proof.add_inference("and", vec![a1, a2], "p /\\ q");
349
350 let compressor = ProofCompressor::new();
351 let (compressed, _) = compressor.compress(&proof);
352
353 assert!(compressed.len() <= proof.len());
355 }
356
357 #[test]
358 fn test_trim_to_conclusion() {
359 let mut proof = Proof::new();
360 let a1 = proof.add_axiom("p");
361 let a2 = proof.add_axiom("q");
362 let a3 = proof.add_axiom("r");
363 let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
364 let _i2 = proof.add_inference("or", vec![a3], "r");
365
366 let trimmed = trim_to_conclusion(&proof, i1);
368
369 assert_eq!(trimmed.len(), 3);
371 }
372
373 #[test]
374 fn test_dependency_cone() {
375 let mut proof = Proof::new();
376 let a1 = proof.add_axiom("p");
377 let a2 = proof.add_axiom("q");
378 let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
379 let i2 = proof.add_inference("not", vec![i1], "~(p /\\ q)");
380
381 let cone = get_dependency_cone(&proof, i2);
382
383 assert_eq!(cone.len(), 4);
385 assert!(cone.contains(&a1));
386 assert!(cone.contains(&a2));
387 assert!(cone.contains(&i1));
388 assert!(cone.contains(&i2));
389 }
390
391 #[test]
392 fn test_compressor_default() {
393 let compressor = ProofCompressor::default();
394 assert!(compressor.config.remove_redundant);
395 assert!(compressor.config.trim_unused);
396 }
397
398 #[test]
399 fn test_compress_simple_proof() {
400 let mut proof = Proof::new();
401 let a1 = proof.add_axiom("p");
402 let a2 = proof.add_axiom("q");
403 proof.add_inference("and", vec![a1, a2], "p /\\ q");
404
405 let compressor = ProofCompressor::new();
406 let (compressed, result) = compressor.compress(&proof);
407
408 assert_eq!(result.nodes_before, proof.len());
409 assert_eq!(result.nodes_after, compressed.len());
410 }
411
412 mod proptests {
414 use super::*;
415 use proptest::prelude::*;
416
417 fn var_name() -> impl Strategy<Value = String> {
418 "[a-z][0-9]*".prop_map(|s| s.to_string())
419 }
420
421 proptest! {
422 #[test]
424 fn prop_compression_reduces_or_maintains_size(
425 conclusions in prop::collection::vec(var_name(), 1..15)
426 ) {
427 let mut proof = Proof::new();
428 for conclusion in &conclusions {
429 proof.add_axiom(conclusion);
430 }
431
432 let compressor = ProofCompressor::new();
433 let (compressed, result) = compressor.compress(&proof);
434
435 prop_assert!(compressed.len() <= proof.len());
436 prop_assert_eq!(result.nodes_before, proof.len());
437 prop_assert_eq!(result.nodes_after, compressed.len());
438 }
439
440 #[test]
442 fn prop_compression_ratio_bounds(
443 conclusions in prop::collection::vec(var_name(), 1..10)
444 ) {
445 let mut proof = Proof::new();
446 for conclusion in &conclusions {
447 proof.add_axiom(conclusion);
448 }
449
450 let compressor = ProofCompressor::new();
451 let (_, result) = compressor.compress(&proof);
452
453 prop_assert!(result.ratio >= 0.0);
454 prop_assert!(result.ratio <= 1.0);
455 }
456
457 #[test]
459 fn prop_nodes_removed_consistency(
460 conclusions in prop::collection::vec(var_name(), 1..12)
461 ) {
462 let mut proof = Proof::new();
463 for conclusion in &conclusions {
464 proof.add_axiom(conclusion);
465 }
466
467 let compressor = ProofCompressor::new();
468 let (_, result) = compressor.compress(&proof);
469
470 prop_assert_eq!(
471 result.nodes_removed,
472 result.nodes_before.saturating_sub(result.nodes_after)
473 );
474 }
475
476 #[test]
478 fn prop_dependency_cone_includes_target(
479 conclusions in prop::collection::vec(var_name(), 2..8)
480 ) {
481 let mut proof = Proof::new();
482 let mut ids = Vec::new();
483
484 for conclusion in &conclusions {
485 ids.push(proof.add_axiom(conclusion));
486 }
487
488 if ids.len() >= 2 {
489 let inf_id = proof.add_inference("and", vec![ids[0], ids[1]], "result");
490 let cone = get_dependency_cone(&proof, inf_id);
491 prop_assert!(cone.contains(&inf_id));
492 }
493 }
494
495 #[test]
497 fn prop_dependency_cone_includes_premises(
498 conclusions in prop::collection::vec(var_name(), 2..8)
499 ) {
500 let mut proof = Proof::new();
501 let mut premise_ids = Vec::new();
502
503 for conclusion in &conclusions {
504 premise_ids.push(proof.add_axiom(conclusion));
505 }
506
507 if premise_ids.len() >= 2 {
508 let inf_id = proof.add_inference(
509 "and",
510 premise_ids[..2].to_vec(),
511 "result"
512 );
513 let cone = get_dependency_cone(&proof, inf_id);
514
515 for &premise_id in &premise_ids[..2] {
516 prop_assert!(cone.contains(&premise_id));
517 }
518 }
519 }
520
521 #[test]
523 fn prop_trim_reduces_size(
524 conclusions in prop::collection::vec(var_name(), 2..10)
525 ) {
526 let mut proof = Proof::new();
527 let mut ids = Vec::new();
528
529 for conclusion in &conclusions {
530 ids.push(proof.add_axiom(conclusion));
531 }
532
533 if ids.len() >= 2 {
534 let target = proof.add_inference("and", vec![ids[0], ids[1]], "target");
535 let trimmed = trim_to_conclusion(&proof, target);
536 prop_assert!(trimmed.len() <= proof.len());
537 }
538 }
539
540 #[test]
542 fn prop_trim_contains_target(
543 conclusions in prop::collection::vec(var_name(), 2..8)
544 ) {
545 let mut proof = Proof::new();
546 let mut ids = Vec::new();
547
548 for conclusion in &conclusions {
549 ids.push(proof.add_axiom(conclusion));
550 }
551
552 if ids.len() >= 2 {
553 let target = proof.add_inference("and", vec![ids[0], ids[1]], "target");
554 let trimmed = trim_to_conclusion(&proof, target);
555
556 prop_assert!(!trimmed.is_empty());
558 }
559 }
560
561 #[test]
563 fn prop_compression_idempotent(
564 conclusions in prop::collection::vec(var_name(), 1..8)
565 ) {
566 let mut proof = Proof::new();
567 for conclusion in &conclusions {
568 proof.add_axiom(conclusion);
569 }
570
571 let compressor = ProofCompressor::new();
572 let (compressed1, _) = compressor.compress(&proof);
573 let (compressed2, _) = compressor.compress(&compressed1);
574
575 prop_assert_eq!(compressed1.len(), compressed2.len());
577 }
578
579 #[test]
581 fn prop_no_optimization_preserves_size(
582 conclusions in prop::collection::vec(var_name(), 1..8)
583 ) {
584 let mut proof = Proof::new();
585 for conclusion in &conclusions {
586 proof.add_axiom(conclusion);
587 }
588
589 let config = CompressionConfig {
590 remove_redundant: false,
591 trim_unused: false,
592 inline_trivial: false,
593 max_passes: 0,
594 };
595 let compressor = ProofCompressor::with_config(config);
596 let (compressed, _) = compressor.compress(&proof);
597
598 prop_assert_eq!(compressed.len(), proof.len());
599 }
600 }
601 }
602}