mod bytes;
mod coarse;
mod deletion;
mod floats;
mod index_passes;
mod integers;
mod mutation;
mod ordering;
mod scheduling;
mod sequence;
mod spans;
mod strings;
pub use scheduling::ShrinkPass;
use std::collections::{HashMap, HashSet};
use crate::native::bignum::BigInt;
use crate::native::core::{
ChoiceKind, ChoiceNode, ChoiceValue, MAX_SHRINKS, NodeSortKey, Spans, sort_key,
};
pub enum ShrinkRun<'a> {
Full(&'a [ChoiceNode]),
Probe {
prefix: &'a [ChoiceValue],
seed: u64,
max_size: usize,
},
}
pub type TestFn<'a> = dyn FnMut(ShrinkRun) -> (bool, Vec<ChoiceNode>, Spans) + 'a;
pub type DebugFn<'a> = dyn FnMut(&str) + 'a;
pub struct Shrinker<'a> {
test_fn: Box<TestFn<'a>>,
pub current_nodes: Vec<ChoiceNode>,
pub current_spans: Spans,
pub improvements: usize,
pub downgraded: Vec<Vec<ChoiceValue>>,
pub max_improvements: usize,
pub calls: usize,
pub calls_at_last_shrink: usize,
pub max_stall: usize,
last_checkpoint_nodes: Vec<ChoiceNode>,
all_changed_nodes: HashSet<usize>,
consider_cache: HashSet<Vec<(u8, NodeSortKey)>>,
pub(super) debug: Option<Box<DebugFn<'a>>>,
}
fn kind_tag(kind: &crate::native::core::ChoiceKind) -> u8 {
use crate::native::core::ChoiceKind::*;
match kind {
Boolean(_) => 0,
Integer(_) => 1,
Float(_) => 2,
Bytes(_) => 3,
String(_) => 4,
}
}
impl<'a> Shrinker<'a> {
pub fn with_probe(
test_fn: Box<TestFn<'a>>,
initial_nodes: Vec<ChoiceNode>,
initial_spans: Spans,
) -> Self {
Shrinker {
test_fn,
last_checkpoint_nodes: initial_nodes.clone(),
current_nodes: initial_nodes,
current_spans: initial_spans,
improvements: 0,
downgraded: Vec::new(),
max_improvements: MAX_SHRINKS,
calls: 0,
calls_at_last_shrink: 0,
max_stall: MAX_SHRINKS,
all_changed_nodes: HashSet::new(),
consider_cache: HashSet::new(),
debug: None,
}
}
pub fn set_debug<F: FnMut(&str) + 'a>(&mut self, f: F) {
self.debug = Some(Box::new(f));
}
pub(super) fn debug_msg(&mut self, msg: &str) {
if let Some(d) = self.debug.as_mut() {
d(msg);
}
}
pub fn consider(&mut self, nodes: &[ChoiceNode]) -> bool {
if sort_key(nodes) == sort_key(&self.current_nodes) {
return true;
}
for (i, candidate) in nodes
.iter()
.enumerate()
.take(nodes.len().min(self.current_nodes.len()))
{
if self.current_nodes[i].was_forced && candidate.value != self.current_nodes[i].value {
return false;
}
}
if self.improvements >= self.max_improvements {
return false;
}
if self.improvements > 0
&& self.calls.saturating_sub(self.calls_at_last_shrink) >= self.max_stall
{
return false;
}
let cache_key: Vec<(u8, NodeSortKey)> = nodes
.iter()
.map(|node| (kind_tag(&node.kind), node.sort_key()))
.collect();
if self.consider_cache.contains(&cache_key) {
return false;
}
self.calls += 1;
let (is_interesting, actual_nodes, actual_spans) = (self.test_fn)(ShrinkRun::Full(nodes));
if !is_interesting {
self.consider_cache.insert(cache_key);
}
if is_interesting && sort_key(&actual_nodes) < sort_key(&self.current_nodes) {
self.accept_improvement(actual_nodes, actual_spans);
}
is_interesting
}
pub(super) fn probe(&mut self, prefix: &[ChoiceValue], seed: u64, max_size: usize) {
if self.improvements >= self.max_improvements {
return;
}
if self.calls.saturating_sub(self.calls_at_last_shrink) >= self.max_stall {
return;
}
self.calls += 1;
let (is_interesting, actual_nodes, actual_spans) = (self.test_fn)(ShrinkRun::Probe {
prefix,
seed,
max_size,
});
if is_interesting && sort_key(&actual_nodes) < sort_key(&self.current_nodes) {
self.accept_improvement(actual_nodes, actual_spans);
}
}
fn accept_improvement(&mut self, new_nodes: Vec<ChoiceNode>, new_spans: Spans) {
let old: Vec<ChoiceValue> = self.current_nodes.iter().map(|n| n.value.clone()).collect();
self.downgraded.push(old);
self.improvements += 1;
let span = self.calls.saturating_sub(self.calls_at_last_shrink);
let grown = span.saturating_mul(2);
if grown > self.max_stall {
self.max_stall = grown;
}
self.calls_at_last_shrink = self.calls;
Self::update_change_tracking(
&self.last_checkpoint_nodes,
&new_nodes,
&mut self.all_changed_nodes,
);
self.current_nodes = new_nodes;
self.current_spans = new_spans;
}
fn update_change_tracking(
prev: &[ChoiceNode],
new: &[ChoiceNode],
changed: &mut HashSet<usize>,
) {
let shape_preserved = prev.len() == new.len()
&& prev.iter().zip(new.iter()).all(|(a, b)| {
std::mem::discriminant(a.kind.as_ref()) == std::mem::discriminant(b.kind.as_ref())
});
if !shape_preserved {
changed.clear();
return;
}
for (i, (a, b)) in prev.iter().zip(new.iter()).enumerate() {
if a.value != b.value {
changed.insert(i);
}
}
}
pub fn changed_nodes(&self) -> &HashSet<usize> {
&self.all_changed_nodes
}
pub fn clear_change_tracking(&mut self) {
self.all_changed_nodes.clear();
self.last_checkpoint_nodes = self.current_nodes.clone();
}
pub fn replace(&mut self, values: &HashMap<usize, ChoiceValue>) -> bool {
let mut attempt: Vec<ChoiceNode> = self.current_nodes.clone();
for (&i, v) in values {
if i >= attempt.len() {
return false;
}
if attempt[i].was_forced {
return false;
}
if !attempt[i].kind.validate(v) {
return false;
}
let coerced = match (attempt[i].kind.as_ref(), v) {
(ChoiceKind::Integer(ic), ChoiceValue::Integer(av)) => ChoiceValue::Integer(
ic.value_from_bigint(av)
.unwrap_or_else(|| unreachable!("validated integer fits the node's width")),
),
_ => v.clone(),
};
attempt[i] = attempt[i].with_value(coerced);
}
self.consider(&attempt)
}
fn emit_profile_report(
&mut self,
passes: &[ShrinkPass<'a>],
initial_size: usize,
initial_calls: usize,
) {
if self.debug.is_none() {
return;
}
fn s(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
let stats = self.pass_stats(passes);
let total_calls = self.calls.saturating_sub(initial_calls);
let total_deleted = initial_size.saturating_sub(self.current_nodes.len());
let shrinks = self.improvements;
self.debug_msg("---------------------");
self.debug_msg("Shrink pass profiling");
self.debug_msg("---------------------");
self.debug_msg("");
self.debug_msg(&format!(
"Shrinking made a total of {total_calls} call{} of which {shrinks} shrank. \
This deleted {total_deleted} choice{} out of {initial_size}.",
s(total_calls),
s(total_deleted),
));
for useful in [true, false] {
self.debug_msg("");
self.debug_msg(if useful {
"Useful passes:"
} else {
"Useless passes:"
});
self.debug_msg("");
let mut buckets: Vec<&(&'static str, usize, usize, usize)> = stats
.iter()
.filter(|(_, calls, shrinks, _)| *calls > 0 && ((*shrinks > 0) == useful))
.collect();
buckets.sort_by_key(|(_, calls, shrinks, deletions)| {
(std::cmp::Reverse(*calls), *deletions, *shrinks)
});
for (name, calls, shrinks, deletions) in buckets {
self.debug_msg(&format!(
" * {name} made {calls} call{} of which {shrinks} shrank, \
deleting {deletions} choice{}.",
s(*calls),
s(*deletions),
));
}
}
self.debug_msg("");
}
pub fn shrink(&mut self) {
let mut passes: Vec<ShrinkPass> = vec![
ShrinkPass::new(
"remove_discarded",
Box::new(|sh| {
sh.remove_discarded();
}),
),
ShrinkPass::new("try_trivial_spans", Box::new(|sh| sh.try_trivial_spans())),
ShrinkPass::new("pass_to_descendant", Box::new(|sh| sh.pass_to_descendant())),
ShrinkPass::new("reorder_spans", Box::new(|sh| sh.reorder_spans())),
ShrinkPass::new("node_program_5", Box::new(|sh| sh.node_program(5))),
ShrinkPass::new("node_program_4", Box::new(|sh| sh.node_program(4))),
ShrinkPass::new("node_program_3", Box::new(|sh| sh.node_program(3))),
ShrinkPass::new("node_program_2", Box::new(|sh| sh.node_program(2))),
ShrinkPass::new("node_program_1", Box::new(|sh| sh.node_program(1))),
ShrinkPass::new("delete_chunks", Box::new(|sh| sh.delete_chunks())),
ShrinkPass::new("zero_choices", Box::new(|sh| sh.zero_choices())),
ShrinkPass::new("swap_integer_sign", Box::new(|sh| sh.swap_integer_sign())),
ShrinkPass::new(
"binary_search_integer_towards_zero",
Box::new(|sh| sh.binary_search_integer_towards_zero()),
),
ShrinkPass::new("bind_deletion", Box::new(|sh| sh.bind_deletion())),
ShrinkPass::new(
"minimize_individual_choices",
Box::new(|sh| sh.minimize_individual_choices()),
),
ShrinkPass::new(
"lower_common_node_offset",
Box::new(|sh| sh.lower_common_node_offset()),
),
ShrinkPass::new(
"redistribute_integers",
Box::new(|sh| sh.redistribute_integers()),
),
ShrinkPass::new(
"lower_integers_together",
Box::new(|sh| sh.lower_integers_together()),
),
ShrinkPass::new("shrink_duplicates", Box::new(|sh| sh.shrink_duplicates())),
ShrinkPass::new("sort_values", Box::new(|sh| sh.sort_values())),
ShrinkPass::new(
"swap_adjacent_blocks",
Box::new(|sh| sh.swap_adjacent_blocks()),
),
ShrinkPass::new("shrink_floats", Box::new(|sh| sh.shrink_floats())),
ShrinkPass::new(
"redistribute_numeric_pairs",
Box::new(|sh| sh.redistribute_numeric_pairs()),
),
ShrinkPass::new("shrink_bytes", Box::new(|sh| sh.shrink_bytes())),
ShrinkPass::new(
"redistribute_bytes_pairs",
Box::new(|sh| sh.redistribute_bytes_pairs()),
),
ShrinkPass::new("shrink_strings", Box::new(|sh| sh.shrink_strings())),
ShrinkPass::new(
"lower_duplicated_characters",
Box::new(|sh| sh.lower_duplicated_characters()),
),
ShrinkPass::new(
"normalize_unicode_chars",
Box::new(|sh| sh.normalize_unicode_chars()),
),
ShrinkPass::new(
"redistribute_string_pairs",
Box::new(|sh| sh.redistribute_string_pairs()),
),
ShrinkPass::new("lower_and_bump", Box::new(|sh| sh.lower_and_bump())),
ShrinkPass::new(
"try_shortening_via_increment",
Box::new(|sh| sh.try_shortening_via_increment()),
),
ShrinkPass::new("mutate_and_shrink", Box::new(|sh| sh.mutate_and_shrink())),
];
let initial_size = self.current_nodes.len();
let initial_calls = self.calls;
self.fixate_shrink_passes(&mut passes);
self.emit_profile_report(&passes, initial_size, initial_calls);
}
}
pub(super) fn bin_search_down(lo: i128, hi: i128, f: &mut impl FnMut(i128) -> bool) -> i128 {
if f(lo) {
return lo;
}
let mut lo = lo;
let mut hi = hi;
while lo.checked_add(1).is_some_and(|n| n < hi) {
let mid = lo + (hi - lo) / 2;
if f(mid) {
hi = mid;
} else {
lo = mid;
}
}
hi
}
pub(super) fn bin_search_down_big(
lo: BigInt,
hi: BigInt,
f: &mut impl FnMut(&BigInt) -> bool,
) -> BigInt {
if f(&lo) {
return lo;
}
let mut lo = lo;
let mut hi = hi;
while &lo + 1 < hi {
let mid = &lo + (&hi - &lo) / 2;
if f(&mid) {
hi = mid;
} else {
lo = mid;
}
}
hi
}
pub(crate) fn find_integer(mut f: impl FnMut(usize) -> bool) -> usize {
for i in 1..5 {
if !f(i) {
return i - 1;
}
}
let mut lo = 4;
let mut hi = 5;
while f(hi) {
lo = hi;
let Some(next) = hi.checked_mul(2) else {
return lo;
};
hi = next;
}
while lo + 1 < hi {
let mid = lo + (hi - lo) / 2;
if f(mid) {
lo = mid;
} else {
hi = mid;
}
}
lo
}
#[cfg(test)]
#[path = "../../../tests/embedded/native/shrinker_spans_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "../../../tests/embedded/native/shrinker_forced_node_tests.rs"]
mod forced_node_tests;
#[cfg(test)]
#[path = "../../../tests/embedded/native/shrinker_cache_tests.rs"]
mod cache_tests;
#[cfg(test)]
#[path = "../../../tests/embedded/native/shrinker_defensive_branch_tests.rs"]
mod defensive_branch_tests;