#![forbid(unsafe_code)]
pub trait Decomposable: Clone {
fn children(&self) -> Vec<Self>;
fn remove_child(&mut self, idx: usize);
fn replace_children(&mut self, new_children: Vec<Self>);
}
pub fn hdd_minimize<T, F>(input: T, predicate: F) -> T
where
T: Decomposable,
F: Fn(&T) -> bool,
{
assert!(
predicate(&input),
"predicate must hold on the original input"
);
hdd_minimize_inner(input, &predicate)
}
fn hdd_minimize_inner<T, F>(mut input: T, predicate: &F) -> T
where
T: Decomposable,
F: Fn(&T) -> bool,
{
input = ddmin_children(input, predicate);
let mut children = input.children();
for i in 0..children.len() {
let minimized = hdd_minimize_inner(children[i].clone(), predicate);
let original = children[i].clone();
children[i] = minimized;
let mut candidate = input.clone();
candidate.replace_children(children.clone());
if predicate(&candidate) {
input = candidate;
} else {
children[i] = original;
}
}
input
}
fn ddmin_children<T, F>(mut input: T, predicate: &F) -> T
where
T: Decomposable,
F: Fn(&T) -> bool,
{
let mut n = 2usize;
loop {
let children = input.children();
let len = children.len();
if len == 0 {
break;
}
let chunk_size = len.div_ceil(n);
let mut reduced = false;
let mut i = 0;
while i < n {
let start = i * chunk_size;
let end = (start + chunk_size).min(len);
if start >= len {
break;
}
let mut candidate = input.clone();
let remaining: Vec<T> = children
.iter()
.enumerate()
.filter(|(idx, _)| *idx < start || *idx >= end)
.map(|(_, c)| c.clone())
.collect();
candidate.replace_children(remaining);
if predicate(&candidate) {
input = candidate;
n = 2;
reduced = true;
break;
}
i += 1;
}
if reduced {
continue;
}
if n > 1 {
let mut i = 0;
while i < n {
let start = i * chunk_size;
let end = (start + chunk_size).min(len);
if start >= len {
break;
}
let kept_len = end - start;
if kept_len >= len {
i += 1;
continue;
}
let mut candidate = input.clone();
let kept: Vec<T> = children[start..end].to_vec();
candidate.replace_children(kept);
if predicate(&candidate) {
input = candidate;
n = 2;
reduced = true;
break;
}
i += 1;
}
}
if reduced {
continue;
}
if n >= len {
break;
}
n = (n * 2).min(len);
}
input
}
use std::cell::RefCell;
use std::fmt;
#[derive(Clone, Debug)]
pub struct ReductionStep {
pub step: usize,
pub phase: ReductionPhase,
pub children_before: usize,
pub children_after: usize,
pub accepted: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReductionPhase {
ChunkRemoval,
ChunkRetention,
ChildRecursion,
}
impl fmt::Display for ReductionPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ChunkRemoval => write!(f, "chunk_removal"),
Self::ChunkRetention => write!(f, "chunk_retention"),
Self::ChildRecursion => write!(f, "child_recursion"),
}
}
}
#[derive(Clone, Debug)]
pub struct MinimizationResult<T> {
pub minimized: T,
pub steps: Vec<ReductionStep>,
pub predicate_calls: usize,
}
impl<T> MinimizationResult<T> {
pub fn steps_to_jsonl(&self) -> String {
let mut out = String::new();
for step in &self.steps {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&format!(
"{{\"step\":{},\"phase\":\"{}\",\"children_before\":{},\"children_after\":{},\"accepted\":{}}}",
step.step, step.phase, step.children_before, step.children_after, step.accepted
));
}
out
}
}
pub fn hdd_minimize_logged<T, F>(input: T, predicate: F) -> MinimizationResult<T>
where
T: Decomposable,
F: Fn(&T) -> bool,
{
let log = RefCell::new(Vec::new());
let call_count = RefCell::new(0usize);
let logging_predicate = |t: &T| -> bool {
*call_count.borrow_mut() += 1;
predicate(t)
};
assert!(
logging_predicate(&input),
"predicate must hold on the original input"
);
let minimized = hdd_logged_inner(input, &logging_predicate, &log);
MinimizationResult {
minimized,
steps: log.into_inner(),
predicate_calls: call_count.into_inner(),
}
}
fn hdd_logged_inner<T, F>(mut input: T, predicate: &F, log: &RefCell<Vec<ReductionStep>>) -> T
where
T: Decomposable,
F: Fn(&T) -> bool,
{
input = ddmin_children_logged(input, predicate, log);
let mut children = input.children();
for i in 0..children.len() {
let before_count = count_children_recursive(&children[i]);
let minimized = hdd_logged_inner(children[i].clone(), predicate, log);
let after_count = count_children_recursive(&minimized);
let original = children[i].clone();
children[i] = minimized;
let mut candidate = input.clone();
candidate.replace_children(children.clone());
let accepted = predicate(&candidate);
let step_num = log.borrow().len();
log.borrow_mut().push(ReductionStep {
step: step_num,
phase: ReductionPhase::ChildRecursion,
children_before: before_count,
children_after: if accepted { after_count } else { before_count },
accepted,
});
if accepted {
input = candidate;
} else {
children[i] = original;
}
}
input
}
fn ddmin_children_logged<T, F>(mut input: T, predicate: &F, log: &RefCell<Vec<ReductionStep>>) -> T
where
T: Decomposable,
F: Fn(&T) -> bool,
{
let mut n = 2usize;
loop {
let children = input.children();
let len = children.len();
if len == 0 {
break;
}
let chunk_size = len.div_ceil(n);
let mut reduced = false;
let mut i = 0;
while i < n {
let start = i * chunk_size;
let end = (start + chunk_size).min(len);
if start >= len {
break;
}
let mut candidate = input.clone();
let remaining: Vec<T> = children
.iter()
.enumerate()
.filter(|(idx, _)| *idx < start || *idx >= end)
.map(|(_, c)| c.clone())
.collect();
let new_len = remaining.len();
candidate.replace_children(remaining);
let accepted = predicate(&candidate);
let step_num = log.borrow().len();
log.borrow_mut().push(ReductionStep {
step: step_num,
phase: ReductionPhase::ChunkRemoval,
children_before: len,
children_after: if accepted { new_len } else { len },
accepted,
});
if accepted {
input = candidate;
n = 2;
reduced = true;
break;
}
i += 1;
}
if reduced {
continue;
}
if n > 1 {
let mut i = 0;
while i < n {
let start = i * chunk_size;
let end = (start + chunk_size).min(len);
if start >= len {
break;
}
let kept_len = end - start;
if kept_len >= len {
i += 1;
continue;
}
let mut candidate = input.clone();
let kept: Vec<T> = children[start..end].to_vec();
candidate.replace_children(kept);
let accepted = predicate(&candidate);
let step_num = log.borrow().len();
log.borrow_mut().push(ReductionStep {
step: step_num,
phase: ReductionPhase::ChunkRetention,
children_before: len,
children_after: if accepted { kept_len } else { len },
accepted,
});
if accepted {
input = candidate;
n = 2;
reduced = true;
break;
}
i += 1;
}
}
if reduced {
continue;
}
if n >= len {
break;
}
n = (n * 2).min(len);
}
input
}
fn count_children_recursive<T: Decomposable>(node: &T) -> usize {
let children = node.children();
children.len() + children.iter().map(count_children_recursive).sum::<usize>()
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq)]
struct Tree {
label: String,
children: Vec<Tree>,
}
impl Tree {
fn leaf(label: &str) -> Self {
Self {
label: label.to_string(),
children: vec![],
}
}
fn node(label: &str, children: Vec<Tree>) -> Self {
Self {
label: label.to_string(),
children,
}
}
}
impl Decomposable for Tree {
fn children(&self) -> Vec<Self> {
self.children.clone()
}
fn remove_child(&mut self, idx: usize) {
self.children.remove(idx);
}
fn replace_children(&mut self, new_children: Vec<Self>) {
self.children = new_children;
}
}
impl<T: Clone> Decomposable for Vec<T> {
fn children(&self) -> Vec<Self> {
if self.len() <= 1 {
return vec![];
}
self.iter().map(|item| vec![item.clone()]).collect()
}
fn remove_child(&mut self, idx: usize) {
self.remove(idx);
}
fn replace_children(&mut self, new_children: Vec<Self>) {
*self = new_children.into_iter().flatten().collect();
}
}
#[test]
fn single_child_preserved() {
let tree = Tree::node("root", vec![Tree::leaf("only")]);
let result = hdd_minimize(tree, |t| t.children.iter().any(|c| c.label == "only"));
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].label, "only");
}
#[test]
fn removes_irrelevant_children() {
let tree = Tree::node(
"root",
vec![
Tree::leaf("a"),
Tree::leaf("b"),
Tree::leaf("trigger"),
Tree::leaf("c"),
Tree::leaf("d"),
],
);
let result = hdd_minimize(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].label, "trigger");
}
#[test]
fn preserves_two_required_children() {
let tree = Tree::node(
"root",
vec![
Tree::leaf("a"),
Tree::leaf("needed1"),
Tree::leaf("b"),
Tree::leaf("needed2"),
Tree::leaf("c"),
],
);
let result = hdd_minimize(tree, |t| {
let labels: Vec<&str> = t.children.iter().map(|c| c.label.as_str()).collect();
labels.contains(&"needed1") && labels.contains(&"needed2")
});
assert_eq!(result.children.len(), 2);
let labels: Vec<&str> = result.children.iter().map(|c| c.label.as_str()).collect();
assert!(labels.contains(&"needed1"));
assert!(labels.contains(&"needed2"));
}
#[test]
fn recurses_into_children() {
let tree = Tree::node(
"root",
vec![Tree::node(
"parent",
vec![Tree::leaf("x"), Tree::leaf("culprit"), Tree::leaf("y")],
)],
);
let result = hdd_minimize(tree, |t| {
fn has_culprit(t: &Tree) -> bool {
if t.label == "culprit" {
return true;
}
t.children.iter().any(has_culprit)
}
has_culprit(t)
});
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].label, "parent");
assert_eq!(result.children[0].children.len(), 1);
assert_eq!(result.children[0].children[0].label, "culprit");
}
#[test]
fn empty_children_is_fixpoint() {
let tree = Tree::leaf("root");
let result = hdd_minimize(tree.clone(), |_| true);
assert_eq!(result, tree);
}
#[test]
fn event_sequence_minimization() {
let events: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8];
let result = hdd_minimize(events, |seq| seq.contains(&3) && seq.contains(&7));
assert!(result.contains(&3));
assert!(result.contains(&7));
assert!(result.len() <= 3); }
#[test]
fn deep_nested_minimization() {
let tree = Tree::node(
"root",
vec![
Tree::node(
"branch1",
vec![
Tree::leaf("noise1"),
Tree::node(
"sub",
vec![Tree::leaf("noise2"), Tree::leaf("deep_trigger")],
),
],
),
Tree::leaf("noise3"),
],
);
let result = hdd_minimize(tree, |t| {
fn find_label(t: &Tree, label: &str) -> bool {
if t.label == label {
return true;
}
t.children.iter().any(|c| find_label(c, label))
}
find_label(t, "deep_trigger")
});
fn find_label(t: &Tree, label: &str) -> bool {
if t.label == label {
return true;
}
t.children.iter().any(|c| find_label(c, label))
}
assert!(find_label(&result, "deep_trigger"));
fn count_nodes(t: &Tree) -> usize {
1 + t.children.iter().map(count_nodes).sum::<usize>()
}
assert!(count_nodes(&result) <= 4);
}
#[test]
#[should_panic(expected = "predicate must hold")]
fn panics_if_predicate_fails_on_input() {
let tree = Tree::leaf("root");
hdd_minimize(tree, |_| false);
}
#[test]
fn all_children_needed() {
let tree = Tree::node(
"root",
vec![Tree::leaf("a"), Tree::leaf("b"), Tree::leaf("c")],
);
let result = hdd_minimize(tree.clone(), |t| t.children.len() == 3);
assert_eq!(result.children.len(), 3);
}
#[test]
fn large_input_binary_search_efficiency() {
let children: Vec<Tree> = (0..100).map(|i| Tree::leaf(&format!("n{i}"))).collect();
let tree = Tree::node("root", children);
let call_count = std::cell::Cell::new(0u64);
let result = hdd_minimize(tree, |t| {
call_count.set(call_count.get() + 1);
t.children.iter().any(|c| c.label == "n42")
});
assert_eq!(result.children.len(), 1);
assert_eq!(result.children[0].label, "n42");
assert!(
call_count.get() < 50,
"too many predicate calls: {}",
call_count.get()
);
}
#[test]
fn logged_minimization_produces_steps() {
let tree = Tree::node(
"root",
vec![
Tree::leaf("a"),
Tree::leaf("trigger"),
Tree::leaf("b"),
Tree::leaf("c"),
],
);
let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
assert_eq!(result.minimized.children.len(), 1);
assert_eq!(result.minimized.children[0].label, "trigger");
assert!(!result.steps.is_empty(), "should have logged steps");
assert!(result.predicate_calls > 0);
}
#[test]
fn logged_steps_contain_accepted_reductions() {
let tree = Tree::node(
"root",
vec![
Tree::leaf("a"),
Tree::leaf("b"),
Tree::leaf("trigger"),
Tree::leaf("c"),
Tree::leaf("d"),
],
);
let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
let accepted_count = result.steps.iter().filter(|s| s.accepted).count();
assert!(
accepted_count > 0,
"at least one reduction must be accepted"
);
for step in result.steps.iter().filter(|s| s.accepted) {
assert!(
step.children_after <= step.children_before,
"accepted step should not increase children"
);
}
}
#[test]
fn jsonl_output_is_valid() {
let tree = Tree::node(
"root",
vec![Tree::leaf("a"), Tree::leaf("trigger"), Tree::leaf("b")],
);
let result = hdd_minimize_logged(tree, |t| t.children.iter().any(|c| c.label == "trigger"));
let jsonl = result.steps_to_jsonl();
assert!(!jsonl.is_empty());
for line in jsonl.lines() {
let parsed: serde_json::Value =
serde_json::from_str(line).expect("each JSONL line must be valid JSON");
assert!(parsed.get("step").is_some());
assert!(parsed.get("phase").is_some());
assert!(parsed.get("accepted").is_some());
}
}
#[test]
fn logged_predicate_count_matches() {
let tree = Tree::node(
"root",
vec![Tree::leaf("a"), Tree::leaf("trigger"), Tree::leaf("b")],
);
let manual_count = std::cell::Cell::new(0u64);
let result = hdd_minimize_logged(tree, |t| {
manual_count.set(manual_count.get() + 1);
t.children.iter().any(|c| c.label == "trigger")
});
assert_eq!(result.predicate_calls, manual_count.get() as usize);
}
}