1#![forbid(unsafe_code)]
2
3pub trait Decomposable: Clone {
64 fn children(&self) -> Vec<Self>;
66
67 fn remove_child(&mut self, idx: usize);
73
74 fn replace_children(&mut self, new_children: Vec<Self>);
76}
77
78pub fn hdd_minimize<T, F>(input: T, predicate: F) -> T
88where
89 T: Decomposable,
90 F: Fn(&T) -> bool,
91{
92 assert!(
93 predicate(&input),
94 "predicate must hold on the original input"
95 );
96 hdd_minimize_inner(input, &predicate)
97}
98
99fn hdd_minimize_inner<T, F>(mut input: T, predicate: &F) -> T
100where
101 T: Decomposable,
102 F: Fn(&T) -> bool,
103{
104 input = ddmin_children(input, predicate);
106
107 let mut children = input.children();
109 for i in 0..children.len() {
110 let minimized = hdd_minimize_inner(children[i].clone(), predicate);
111
112 let original = children[i].clone();
114 children[i] = minimized;
115
116 let mut candidate = input.clone();
117 candidate.replace_children(children.clone());
118
119 if predicate(&candidate) {
120 input = candidate;
121 } else {
122 children[i] = original;
124 }
125 }
126
127 input
128}
129
130fn ddmin_children<T, F>(mut input: T, predicate: &F) -> T
135where
136 T: Decomposable,
137 F: Fn(&T) -> bool,
138{
139 let mut n = 2usize;
140
141 loop {
142 let children = input.children();
143 let len = children.len();
144
145 if len == 0 {
146 break;
147 }
148
149 let chunk_size = len.div_ceil(n);
150 let mut reduced = false;
151
152 let mut i = 0;
154 while i < n {
155 let start = i * chunk_size;
156 let end = (start + chunk_size).min(len);
157 if start >= len {
158 break;
159 }
160
161 let mut candidate = input.clone();
163 let remaining: Vec<T> = children
164 .iter()
165 .enumerate()
166 .filter(|(idx, _)| *idx < start || *idx >= end)
167 .map(|(_, c)| c.clone())
168 .collect();
169 candidate.replace_children(remaining);
170
171 if predicate(&candidate) {
172 input = candidate;
173 n = 2;
174 reduced = true;
175 break;
176 }
177 i += 1;
178 }
179
180 if reduced {
181 continue;
182 }
183
184 if n > 1 {
187 let mut i = 0;
188 while i < n {
189 let start = i * chunk_size;
190 let end = (start + chunk_size).min(len);
191 if start >= len {
192 break;
193 }
194
195 let kept_len = end - start;
196 if kept_len >= len {
198 i += 1;
199 continue;
200 }
201
202 let mut candidate = input.clone();
204 let kept: Vec<T> = children[start..end].to_vec();
205 candidate.replace_children(kept);
206
207 if predicate(&candidate) {
208 input = candidate;
209 n = 2;
210 reduced = true;
211 break;
212 }
213 i += 1;
214 }
215 }
216
217 if reduced {
218 continue;
219 }
220
221 if n >= len {
223 break;
224 }
225 n = (n * 2).min(len);
226 }
227
228 input
229}
230
231use std::cell::RefCell;
236use std::fmt;
237
238#[derive(Clone, Debug)]
240pub struct ReductionStep {
241 pub step: usize,
243 pub phase: ReductionPhase,
245 pub children_before: usize,
247 pub children_after: usize,
249 pub accepted: bool,
251}
252
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255pub enum ReductionPhase {
256 ChunkRemoval,
258 ChunkRetention,
260 ChildRecursion,
262}
263
264impl fmt::Display for ReductionPhase {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::ChunkRemoval => write!(f, "chunk_removal"),
268 Self::ChunkRetention => write!(f, "chunk_retention"),
269 Self::ChildRecursion => write!(f, "child_recursion"),
270 }
271 }
272}
273
274#[derive(Clone, Debug)]
276pub struct MinimizationResult<T> {
277 pub minimized: T,
279 pub steps: Vec<ReductionStep>,
281 pub predicate_calls: usize,
283}
284
285impl<T> MinimizationResult<T> {
286 pub fn steps_to_jsonl(&self) -> String {
288 let mut out = String::new();
289 for step in &self.steps {
290 if !out.is_empty() {
291 out.push('\n');
292 }
293 out.push_str(&format!(
294 "{{\"step\":{},\"phase\":\"{}\",\"children_before\":{},\"children_after\":{},\"accepted\":{}}}",
295 step.step, step.phase, step.children_before, step.children_after, step.accepted
296 ));
297 }
298 out
299 }
300}
301
302pub fn hdd_minimize_logged<T, F>(input: T, predicate: F) -> MinimizationResult<T>
311where
312 T: Decomposable,
313 F: Fn(&T) -> bool,
314{
315 let log = RefCell::new(Vec::new());
316 let call_count = RefCell::new(0usize);
317
318 let logging_predicate = |t: &T| -> bool {
319 *call_count.borrow_mut() += 1;
320 predicate(t)
321 };
322
323 assert!(
324 logging_predicate(&input),
325 "predicate must hold on the original input"
326 );
327
328 let minimized = hdd_logged_inner(input, &logging_predicate, &log);
329
330 MinimizationResult {
331 minimized,
332 steps: log.into_inner(),
333 predicate_calls: call_count.into_inner(),
334 }
335}
336
337fn hdd_logged_inner<T, F>(mut input: T, predicate: &F, log: &RefCell<Vec<ReductionStep>>) -> T
338where
339 T: Decomposable,
340 F: Fn(&T) -> bool,
341{
342 input = ddmin_children_logged(input, predicate, log);
344
345 let mut children = input.children();
347 for i in 0..children.len() {
348 let before_count = count_children_recursive(&children[i]);
349 let minimized = hdd_logged_inner(children[i].clone(), predicate, log);
350 let after_count = count_children_recursive(&minimized);
351
352 let original = children[i].clone();
353 children[i] = minimized;
354
355 let mut candidate = input.clone();
356 candidate.replace_children(children.clone());
357
358 let accepted = predicate(&candidate);
359
360 let step_num = log.borrow().len();
361 log.borrow_mut().push(ReductionStep {
362 step: step_num,
363 phase: ReductionPhase::ChildRecursion,
364 children_before: before_count,
365 children_after: if accepted { after_count } else { before_count },
366 accepted,
367 });
368
369 if accepted {
370 input = candidate;
371 } else {
372 children[i] = original;
373 }
374 }
375
376 input
377}
378
379fn ddmin_children_logged<T, F>(mut input: T, predicate: &F, log: &RefCell<Vec<ReductionStep>>) -> T
380where
381 T: Decomposable,
382 F: Fn(&T) -> bool,
383{
384 let mut n = 2usize;
385
386 loop {
387 let children = input.children();
388 let len = children.len();
389
390 if len == 0 {
391 break;
392 }
393
394 let chunk_size = len.div_ceil(n);
395 let mut reduced = false;
396
397 let mut i = 0;
399 while i < n {
400 let start = i * chunk_size;
401 let end = (start + chunk_size).min(len);
402 if start >= len {
403 break;
404 }
405
406 let mut candidate = input.clone();
407 let remaining: Vec<T> = children
408 .iter()
409 .enumerate()
410 .filter(|(idx, _)| *idx < start || *idx >= end)
411 .map(|(_, c)| c.clone())
412 .collect();
413 let new_len = remaining.len();
414 candidate.replace_children(remaining);
415
416 let accepted = predicate(&candidate);
417
418 let step_num = log.borrow().len();
419 log.borrow_mut().push(ReductionStep {
420 step: step_num,
421 phase: ReductionPhase::ChunkRemoval,
422 children_before: len,
423 children_after: if accepted { new_len } else { len },
424 accepted,
425 });
426
427 if accepted {
428 input = candidate;
429 n = 2;
430 reduced = true;
431 break;
432 }
433 i += 1;
434 }
435
436 if reduced {
437 continue;
438 }
439
440 if n > 1 {
442 let mut i = 0;
443 while i < n {
444 let start = i * chunk_size;
445 let end = (start + chunk_size).min(len);
446 if start >= len {
447 break;
448 }
449
450 let kept_len = end - start;
451 if kept_len >= len {
452 i += 1;
453 continue;
454 }
455
456 let mut candidate = input.clone();
457 let kept: Vec<T> = children[start..end].to_vec();
458 candidate.replace_children(kept);
459
460 let accepted = predicate(&candidate);
461
462 let step_num = log.borrow().len();
463 log.borrow_mut().push(ReductionStep {
464 step: step_num,
465 phase: ReductionPhase::ChunkRetention,
466 children_before: len,
467 children_after: if accepted { kept_len } else { len },
468 accepted,
469 });
470
471 if accepted {
472 input = candidate;
473 n = 2;
474 reduced = true;
475 break;
476 }
477 i += 1;
478 }
479 }
480
481 if reduced {
482 continue;
483 }
484
485 if n >= len {
486 break;
487 }
488 n = (n * 2).min(len);
489 }
490
491 input
492}
493
494fn count_children_recursive<T: Decomposable>(node: &T) -> usize {
496 let children = node.children();
497 children.len() + children.iter().map(count_children_recursive).sum::<usize>()
498}
499
500#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[derive(Clone, Debug, PartialEq)]
509 struct Tree {
510 label: String,
511 children: Vec<Tree>,
512 }
513
514 impl Tree {
515 fn leaf(label: &str) -> Self {
516 Self {
517 label: label.to_string(),
518 children: vec![],
519 }
520 }
521
522 fn node(label: &str, children: Vec<Tree>) -> Self {
523 Self {
524 label: label.to_string(),
525 children,
526 }
527 }
528 }
529
530 impl Decomposable for Tree {
531 fn children(&self) -> Vec<Self> {
532 self.children.clone()
533 }
534
535 fn remove_child(&mut self, idx: usize) {
536 self.children.remove(idx);
537 }
538
539 fn replace_children(&mut self, new_children: Vec<Self>) {
540 self.children = new_children;
541 }
542 }
543
544 impl<T: Clone> Decomposable for Vec<T> {
547 fn children(&self) -> Vec<Self> {
548 if self.len() <= 1 {
549 return vec![];
550 }
551 self.iter().map(|item| vec![item.clone()]).collect()
552 }
553
554 fn remove_child(&mut self, idx: usize) {
555 self.remove(idx);
556 }
557
558 fn replace_children(&mut self, new_children: Vec<Self>) {
559 *self = new_children.into_iter().flatten().collect();
560 }
561 }
562
563 #[test]
564 fn single_child_preserved() {
565 let tree = Tree::node("root", vec![Tree::leaf("only")]);
566 let result = hdd_minimize(tree, |t| t.children.iter().any(|c| c.label == "only"));
567 assert_eq!(result.children.len(), 1);
568 assert_eq!(result.children[0].label, "only");
569 }
570
571 #[test]
572 fn removes_irrelevant_children() {
573 let tree = Tree::node(
574 "root",
575 vec![
576 Tree::leaf("a"),
577 Tree::leaf("b"),
578 Tree::leaf("trigger"),
579 Tree::leaf("c"),
580 Tree::leaf("d"),
581 ],
582 );
583
584 let result = hdd_minimize(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
585
586 assert_eq!(result.children.len(), 1);
587 assert_eq!(result.children[0].label, "trigger");
588 }
589
590 #[test]
591 fn preserves_two_required_children() {
592 let tree = Tree::node(
593 "root",
594 vec![
595 Tree::leaf("a"),
596 Tree::leaf("needed1"),
597 Tree::leaf("b"),
598 Tree::leaf("needed2"),
599 Tree::leaf("c"),
600 ],
601 );
602
603 let result = hdd_minimize(tree, |t| {
604 let labels: Vec<&str> = t.children.iter().map(|c| c.label.as_str()).collect();
605 labels.contains(&"needed1") && labels.contains(&"needed2")
606 });
607
608 assert_eq!(result.children.len(), 2);
609 let labels: Vec<&str> = result.children.iter().map(|c| c.label.as_str()).collect();
610 assert!(labels.contains(&"needed1"));
611 assert!(labels.contains(&"needed2"));
612 }
613
614 #[test]
615 fn recurses_into_children() {
616 let tree = Tree::node(
617 "root",
618 vec![Tree::node(
619 "parent",
620 vec![Tree::leaf("x"), Tree::leaf("culprit"), Tree::leaf("y")],
621 )],
622 );
623
624 let result = hdd_minimize(tree, |t| {
625 fn has_culprit(t: &Tree) -> bool {
626 if t.label == "culprit" {
627 return true;
628 }
629 t.children.iter().any(has_culprit)
630 }
631 has_culprit(t)
632 });
633
634 assert_eq!(result.children.len(), 1);
636 assert_eq!(result.children[0].label, "parent");
637 assert_eq!(result.children[0].children.len(), 1);
638 assert_eq!(result.children[0].children[0].label, "culprit");
639 }
640
641 #[test]
642 fn empty_children_is_fixpoint() {
643 let tree = Tree::leaf("root");
644 let result = hdd_minimize(tree.clone(), |_| true);
645 assert_eq!(result, tree);
646 }
647
648 #[test]
649 fn event_sequence_minimization() {
650 let events: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8];
651
652 let result = hdd_minimize(events, |seq| seq.contains(&3) && seq.contains(&7));
654
655 assert!(result.contains(&3));
656 assert!(result.contains(&7));
657 assert!(result.len() <= 3); }
659
660 #[test]
661 fn deep_nested_minimization() {
662 let tree = Tree::node(
663 "root",
664 vec![
665 Tree::node(
666 "branch1",
667 vec![
668 Tree::leaf("noise1"),
669 Tree::node(
670 "sub",
671 vec![Tree::leaf("noise2"), Tree::leaf("deep_trigger")],
672 ),
673 ],
674 ),
675 Tree::leaf("noise3"),
676 ],
677 );
678
679 let result = hdd_minimize(tree, |t| {
680 fn find_label(t: &Tree, label: &str) -> bool {
681 if t.label == label {
682 return true;
683 }
684 t.children.iter().any(|c| find_label(c, label))
685 }
686 find_label(t, "deep_trigger")
687 });
688
689 fn find_label(t: &Tree, label: &str) -> bool {
691 if t.label == label {
692 return true;
693 }
694 t.children.iter().any(|c| find_label(c, label))
695 }
696 assert!(find_label(&result, "deep_trigger"));
697
698 fn count_nodes(t: &Tree) -> usize {
700 1 + t.children.iter().map(count_nodes).sum::<usize>()
701 }
702 assert!(count_nodes(&result) <= 4);
703 }
704
705 #[test]
706 #[should_panic(expected = "predicate must hold")]
707 fn panics_if_predicate_fails_on_input() {
708 let tree = Tree::leaf("root");
709 hdd_minimize(tree, |_| false);
710 }
711
712 #[test]
713 fn all_children_needed() {
714 let tree = Tree::node(
715 "root",
716 vec![Tree::leaf("a"), Tree::leaf("b"), Tree::leaf("c")],
717 );
718
719 let result = hdd_minimize(tree.clone(), |t| t.children.len() == 3);
720
721 assert_eq!(result.children.len(), 3);
722 }
723
724 #[test]
725 fn large_input_binary_search_efficiency() {
726 let children: Vec<Tree> = (0..100).map(|i| Tree::leaf(&format!("n{i}"))).collect();
728 let tree = Tree::node("root", children);
729
730 let call_count = std::cell::Cell::new(0u64);
731 let result = hdd_minimize(tree, |t| {
732 call_count.set(call_count.get() + 1);
733 t.children.iter().any(|c| c.label == "n42")
734 });
735
736 assert_eq!(result.children.len(), 1);
737 assert_eq!(result.children[0].label, "n42");
738
739 assert!(
742 call_count.get() < 50,
743 "too many predicate calls: {}",
744 call_count.get()
745 );
746 }
747
748 #[test]
753 fn logged_minimization_produces_steps() {
754 let tree = Tree::node(
755 "root",
756 vec![
757 Tree::leaf("a"),
758 Tree::leaf("trigger"),
759 Tree::leaf("b"),
760 Tree::leaf("c"),
761 ],
762 );
763
764 let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
765
766 assert_eq!(result.minimized.children.len(), 1);
767 assert_eq!(result.minimized.children[0].label, "trigger");
768 assert!(!result.steps.is_empty(), "should have logged steps");
769 assert!(result.predicate_calls > 0);
770 }
771
772 #[test]
773 fn logged_steps_contain_accepted_reductions() {
774 let tree = Tree::node(
775 "root",
776 vec![
777 Tree::leaf("a"),
778 Tree::leaf("b"),
779 Tree::leaf("trigger"),
780 Tree::leaf("c"),
781 Tree::leaf("d"),
782 ],
783 );
784
785 let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
786
787 let accepted_count = result.steps.iter().filter(|s| s.accepted).count();
789 assert!(
790 accepted_count > 0,
791 "at least one reduction must be accepted"
792 );
793
794 for step in result.steps.iter().filter(|s| s.accepted) {
796 assert!(
797 step.children_after <= step.children_before,
798 "accepted step should not increase children"
799 );
800 }
801 }
802
803 #[test]
804 fn jsonl_output_is_valid() {
805 let tree = Tree::node(
806 "root",
807 vec![Tree::leaf("a"), Tree::leaf("trigger"), Tree::leaf("b")],
808 );
809
810 let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
811
812 let jsonl = result.steps_to_jsonl();
813 assert!(!jsonl.is_empty());
814
815 for line in jsonl.lines() {
817 let parsed: serde_json::Value =
818 serde_json::from_str(line).expect("each JSONL line must be valid JSON");
819 assert!(parsed.get("step").is_some());
820 assert!(parsed.get("phase").is_some());
821 assert!(parsed.get("accepted").is_some());
822 }
823 }
824
825 #[test]
826 fn logged_predicate_count_matches() {
827 let tree = Tree::node(
828 "root",
829 vec![Tree::leaf("a"), Tree::leaf("trigger"), Tree::leaf("b")],
830 );
831
832 let manual_count = std::cell::Cell::new(0u64);
833 let result = hdd_minimize_logged(tree, |t| {
834 manual_count.set(manual_count.get() + 1);
835 t.children.iter().any(|c| c.label == "trigger")
836 });
837
838 assert_eq!(result.predicate_calls, manual_count.get() as usize);
840 }
841}