use amberfork_model::Recovery;
use std::future::Future;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Reduction {
Minimized(Vec<usize>),
Persisted,
Inconclusive,
}
pub(crate) async fn minimize<F, Fut, E>(n: usize, mut recovers: F) -> Result<Reduction, E>
where
F: FnMut(&[usize]) -> Fut,
Fut: Future<Output = Result<Recovery, E>>,
{
if n == 0 {
return Ok(Reduction::Persisted);
}
let mut subset: Vec<usize> = (0..n).collect();
match recovers(&subset).await? {
Recovery::Recovered => {}
Recovery::NotRecovered => return Ok(Reduction::Persisted),
Recovery::Unverified => return Ok(Reduction::Inconclusive),
}
let mut granularity = 2;
while subset.len() >= 2 {
let blocks = partition(&subset, granularity);
let mut sufficient_block = None;
for block in &blocks {
if recovers(block).await? == Recovery::Recovered {
sufficient_block = Some(block.clone());
break;
}
}
if let Some(block) = sufficient_block {
subset = block;
granularity = 2;
continue;
}
let mut reduced = false;
for block in &blocks {
let complement = complement(&subset, block);
if recovers(&complement).await? == Recovery::Recovered {
subset = complement;
granularity = (granularity - 1).max(2);
reduced = true;
break;
}
}
if reduced {
continue;
}
if granularity >= subset.len() {
break;
}
granularity = (granularity * 2).min(subset.len());
}
Ok(Reduction::Minimized(subset))
}
fn partition(subset: &[usize], k: usize) -> Vec<Vec<usize>> {
let len = subset.len();
let k = k.clamp(1, len);
(0..k)
.map(|i| subset[i * len / k..(i + 1) * len / k].to_vec())
.collect()
}
fn complement(subset: &[usize], block: &[usize]) -> Vec<usize> {
subset
.iter()
.filter(|element| !block.contains(element))
.copied()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use amberfork_model::Recovery::{NotRecovered, Recovered, Unverified};
use std::cell::Cell;
use std::convert::Infallible;
fn ceil_log2(n: usize) -> u32 {
if n <= 1 {
0
} else {
usize::BITS - (n - 1).leading_zeros()
}
}
#[tokio::test]
async fn finds_a_single_element_minimal_cause() {
let k = 5;
let got = minimize(8, |subset: &[usize]| {
let recovered = subset.contains(&k);
async move { Ok::<_, Infallible>(if recovered { Recovered } else { NotRecovered }) }
})
.await
.unwrap();
assert_eq!(got, Reduction::Minimized(vec![k]));
}
#[tokio::test]
async fn finds_a_multi_element_minimal_cause() {
let causes = [2usize, 6];
let got = minimize(8, |subset: &[usize]| {
let recovered = causes.iter().all(|c| subset.contains(c));
async move { Ok::<_, Infallible>(if recovered { Recovered } else { NotRecovered }) }
})
.await
.unwrap();
assert_eq!(got, Reduction::Minimized(vec![2, 6]));
}
#[tokio::test]
async fn stays_within_the_logarithmic_rerun_bound() {
let n = 16;
let k = 11;
let calls = Cell::new(0usize);
let got = minimize(n, |subset: &[usize]| {
calls.set(calls.get() + 1);
let recovered = subset.contains(&k);
async move { Ok::<_, Infallible>(if recovered { Recovered } else { NotRecovered }) }
})
.await
.unwrap();
assert_eq!(got, Reduction::Minimized(vec![k]));
assert!(
calls.get() as u32 <= 3 * ceil_log2(n),
"took {} oracle calls, bound is {}",
calls.get(),
3 * ceil_log2(n)
);
}
#[tokio::test]
async fn an_inconclusive_full_set_yields_no_cause() {
let got = minimize(8, |_: &[usize]| async { Ok::<_, Infallible>(Unverified) })
.await
.unwrap();
assert_eq!(got, Reduction::Inconclusive);
}
#[tokio::test]
async fn a_fork_that_never_recovers_persists() {
let got = minimize(8, |_: &[usize]| async { Ok::<_, Infallible>(NotRecovered) })
.await
.unwrap();
assert_eq!(got, Reduction::Persisted);
}
}