use std::collections::BTreeMap;
use crate::candidate::{CandidatePair, StatementRun};
use crate::features::FeatureKind;
use crate::ir::ByteRange;
pub const MAXIMAL_VERSION: &str = "maximal-v1";
pub const DEFAULT_MIN_STATEMENTS: u32 = shortest_window();
#[allow(clippy::cast_possible_truncation)]
const fn shortest_window() -> u32 {
let mut shortest = usize::MAX;
let mut index = 0;
while index < crate::features::WINDOW_LENGTHS.len() {
if crate::features::WINDOW_LENGTHS[index] < shortest {
shortest = crate::features::WINDOW_LENGTHS[index];
}
index += 1;
}
shortest as u32
}
pub const DEFAULT_MAX_EXTENT_RATIO: f64 = 2.0;
#[derive(Debug, Clone, PartialEq)]
pub struct MaximalConfig {
pub min_statements: u32,
pub max_extent_ratio: f64,
}
impl Default for MaximalConfig {
fn default() -> Self {
Self {
min_statements: DEFAULT_MIN_STATEMENTS,
max_extent_ratio: DEFAULT_MAX_EXTENT_RATIO,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct RegionSide {
pub file: usize,
pub unit: usize,
pub run: StatementRun,
pub range: ByteRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct CloneRegion {
pub a: RegionSide,
pub b: RegionSide,
pub seeds: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SharedRegion {
pub occurrences: Vec<RegionSide>,
pub statements: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RegionStats {
pub seeds: usize,
pub divergent_extent: usize,
pub folded: usize,
pub absorbed: usize,
pub self_overlapping: usize,
pub below_minimum: usize,
pub regions: usize,
pub shared: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RegionSet {
pub regions: Vec<CloneRegion>,
pub shared: Vec<SharedRegion>,
pub stats: RegionStats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Alignment {
a_file: usize,
a_unit: usize,
a_block: u32,
b_file: usize,
b_unit: usize,
b_block: u32,
shift: i64,
}
#[derive(Debug, Clone, Copy)]
struct Growing {
a_start: u32,
a_end: u32,
a_bytes: ByteRange,
b_bytes: ByteRange,
seeds: usize,
}
#[must_use]
pub fn consolidate(pairs: &[CandidatePair], config: &MaximalConfig) -> RegionSet {
let mut stats = RegionStats::default();
let mut runs: BTreeMap<Alignment, Vec<(StatementRun, ByteRange, StatementRun, ByteRange)>> =
BTreeMap::new();
for pair in pairs {
if pair.kind != FeatureKind::StatementWindow {
continue;
}
let (Some(a_run), Some(b_run)) = (pair.a.run, pair.b.run) else {
continue;
};
stats.seeds += 1;
let (a_bytes, b_bytes) = pair_ranges(pair);
if diverges(a_bytes, b_bytes, config.max_extent_ratio) {
stats.divergent_extent += 1;
continue;
}
let alignment = Alignment {
a_file: pair.a.file,
a_unit: pair.a.unit,
a_block: a_run.block,
b_file: pair.b.file,
b_unit: pair.b.unit,
b_block: b_run.block,
shift: i64::from(b_run.start) - i64::from(a_run.start),
};
runs.entry(alignment)
.or_default()
.push((a_run, a_bytes, b_run, b_bytes));
}
let mut folded: Vec<CloneRegion> = Vec::new();
for (alignment, mut seeds) in runs {
seeds.sort_by_key(|&(a_run, _, _, _)| (a_run.start, a_run.length));
let mut current: Option<Growing> = None;
for (a_run, a_bytes, _, b_bytes) in seeds {
let extends = current.is_some_and(|growing| a_run.start <= growing.a_end);
if let (true, Some(growing)) = (extends, current.as_mut()) {
growing.a_end = growing.a_end.max(a_run.end());
growing.a_bytes = union(growing.a_bytes, a_bytes);
growing.b_bytes = union(growing.b_bytes, b_bytes);
growing.seeds += 1;
continue;
}
if let Some(done) = current.take() {
folded.push(emit(&alignment, &done));
}
current = Some(Growing {
a_start: a_run.start,
a_end: a_run.end(),
a_bytes,
b_bytes,
seeds: 1,
});
}
if let Some(done) = current {
folded.push(emit(&alignment, &done));
}
}
stats.folded = folded.len();
let mut buckets: BTreeMap<(usize, usize), Vec<CloneRegion>> = BTreeMap::new();
for region in folded {
if region.a.run.length < config.min_statements {
stats.below_minimum += 1;
continue;
}
if overlaps_itself(®ion) {
stats.self_overlapping += 1;
continue;
}
buckets
.entry((region.a.file, region.b.file))
.or_default()
.push(region);
}
let mut kept: Vec<CloneRegion> = Vec::new();
for bucket in buckets.into_values() {
let (bucket, absorbed) = remove_contained(bucket);
stats.absorbed += absorbed;
kept.extend(bucket);
}
kept.sort_unstable();
stats.regions = kept.len();
let shared = share(&kept);
stats.shared = shared.len();
RegionSet {
regions: kept,
shared,
stats,
}
}
const fn pair_ranges(pair: &CandidatePair) -> (ByteRange, ByteRange) {
(
ByteRange {
start: pair.a.start_byte,
end: pair.a.end_byte,
},
ByteRange {
start: pair.b.start_byte,
end: pair.b.end_byte,
},
)
}
fn remove_contained(mut regions: Vec<CloneRegion>) -> (Vec<CloneRegion>, usize) {
regions.sort_by_key(|region| {
(
region.a.range.start,
std::cmp::Reverse(region.a.range.end),
region.b.range.start,
std::cmp::Reverse(region.b.range.end),
region.a,
region.b,
)
});
let mut index = ContainmentIndex::for_regions(®ions);
let mut kept = Vec::with_capacity(regions.len());
let mut absorbed = 0;
for region in regions {
if index.contains(®ion) {
absorbed += 1;
} else {
index.insert(®ion);
kept.push(region);
}
}
(kept, absorbed)
}
struct ContainmentIndex {
second_starts: Vec<usize>,
first_ends: Vec<Vec<usize>>,
greatest_second_ends: Vec<Vec<usize>>,
}
impl ContainmentIndex {
fn for_regions(regions: &[CloneRegion]) -> Self {
let mut second_starts: Vec<usize> =
regions.iter().map(|region| region.b.range.start).collect();
second_starts.sort_unstable();
second_starts.dedup();
let mut first_ends = vec![Vec::new(); second_starts.len() + 1];
for region in regions {
let mut node = second_starts.partition_point(|&start| start < region.b.range.start) + 1;
while node < first_ends.len() {
first_ends[node].push(region.a.range.end);
node += lowbit(node);
}
}
for ends in &mut first_ends {
ends.sort_unstable();
ends.dedup();
}
let greatest_second_ends = first_ends
.iter()
.map(|ends| vec![0; ends.len() + 1])
.collect();
Self {
second_starts,
first_ends,
greatest_second_ends,
}
}
fn insert(&mut self, region: &CloneRegion) {
let mut node = self
.second_starts
.partition_point(|&start| start < region.b.range.start)
+ 1;
while node < self.first_ends.len() {
let ends = &self.first_ends[node];
let reversed = ends.len() - ends.partition_point(|&end| end < region.a.range.end);
let values = &mut self.greatest_second_ends[node];
let mut position = reversed;
while position < values.len() {
values[position] = values[position].max(region.b.range.end);
position += lowbit(position);
}
node += lowbit(node);
}
}
fn contains(&self, region: &CloneRegion) -> bool {
let mut node = self
.second_starts
.partition_point(|&start| start <= region.b.range.start);
while node > 0 {
let ends = &self.first_ends[node];
let reversed = ends.len() - ends.partition_point(|&end| end < region.a.range.end);
let values = &self.greatest_second_ends[node];
let mut greatest = 0;
let mut position = reversed;
while position > 0 {
greatest = greatest.max(values[position]);
position -= lowbit(position);
}
if greatest >= region.b.range.end {
return true;
}
node -= lowbit(node);
}
false
}
}
const fn lowbit(index: usize) -> usize {
index & index.wrapping_neg()
}
fn share(regions: &[CloneRegion]) -> Vec<SharedRegion> {
let mut index: BTreeMap<RegionSide, usize> = BTreeMap::new();
for region in regions {
let next = index.len();
index.entry(region.a).or_insert(next);
let next = index.len();
index.entry(region.b).or_insert(next);
}
let mut parent: Vec<usize> = (0..index.len()).collect();
for region in regions {
let (Some(&a), Some(&b)) = (index.get(®ion.a), index.get(®ion.b)) else {
continue;
};
join(&mut parent, a, b);
}
let mut sets: BTreeMap<usize, Vec<RegionSide>> = BTreeMap::new();
for (&side, &node) in &index {
sets.entry(find(&mut parent, node)).or_default().push(side);
}
let mut shared: Vec<SharedRegion> = sets
.into_values()
.filter(|occurrences| occurrences.len() >= 2)
.map(|mut occurrences| {
occurrences.sort_unstable();
let statements = occurrences.first().map_or(0, |side| side.run.length);
SharedRegion {
occurrences,
statements,
}
})
.collect();
shared.sort_unstable();
shared
}
fn find(parent: &mut [usize], mut node: usize) -> usize {
while parent[node] != node {
parent[node] = parent[parent[node]];
node = parent[node];
}
node
}
fn join(parent: &mut [usize], a: usize, b: usize) {
let (a, b) = (find(parent, a), find(parent, b));
if a != b {
parent[a.max(b)] = a.min(b);
}
}
fn emit(alignment: &Alignment, grown: &Growing) -> CloneRegion {
let length = grown.a_end - grown.a_start;
let b_start = u32::try_from(i64::from(grown.a_start) + alignment.shift).unwrap_or(0);
CloneRegion {
a: RegionSide {
file: alignment.a_file,
unit: alignment.a_unit,
run: StatementRun {
block: alignment.a_block,
start: grown.a_start,
length,
},
range: grown.a_bytes,
},
b: RegionSide {
file: alignment.b_file,
unit: alignment.b_unit,
run: StatementRun {
block: alignment.b_block,
start: b_start,
length,
},
range: grown.b_bytes,
},
seeds: grown.seeds,
}
}
const fn overlaps_itself(region: &CloneRegion) -> bool {
region.a.file == region.b.file && intersects(region.a.range, region.b.range)
}
#[must_use]
pub const fn intersects(a: ByteRange, b: ByteRange) -> bool {
a.start < b.end && b.start < a.end
}
#[must_use]
pub const fn adjoins(a: &RegionSide, b: &RegionSide) -> bool {
a.file == b.file
&& a.unit == b.unit
&& a.run.block == b.run.block
&& (a.run.end() == b.run.start || b.run.end() == a.run.start)
}
fn diverges(a: ByteRange, b: ByteRange, ratio: f64) -> bool {
let (short, long) = {
let (a, b) = (a.len(), b.len());
if a <= b { (a, b) } else { (b, a) }
};
if short == 0 {
return false;
}
#[allow(clippy::cast_precision_loss)]
let measured = long as f64 / short as f64;
measured > ratio
}
const fn union(a: ByteRange, b: ByteRange) -> ByteRange {
ByteRange {
start: if a.start < b.start { a.start } else { b.start },
end: if a.end > b.end { a.end } else { b.end },
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests;