use super::{
BTreeMap, BTreeSet, BuildVariant, ByteRange, CloneClass, ContentNorm, FileContext,
FragmentFingerprint, LiteralNorm, RegionOccurrence, RegionSide, SharedRegion, StructuralRegion,
SyntaxIrFile, Token, line_range, maximal, stable_id,
};
pub(super) fn confirm_regions(
candidates: &[SharedRegion],
files: &[SyntaxIrFile],
offsets: &[usize],
variant: &BuildVariant,
literals: LiteralNorm,
) -> (Vec<Confirmed>, Dropped) {
let mut regions = Vec::new();
let mut dropped = Dropped::default();
for candidate in candidates {
let mut classes: BTreeMap<FragmentFingerprint, Vec<(RegionOccurrence, RegionSide)>> =
BTreeMap::new();
for &side in &candidate.occurrences {
let Some((occurrence, normalized)) =
resolve_occurrence(side, files, offsets, variant, literals)
else {
dropped.singletons += 1;
continue;
};
classes
.entry(normalized)
.or_default()
.push((occurrence, side));
}
for (normalized_content, class) in classes {
let class = distinct(class, &mut dropped);
if class.len() < 2 {
dropped.singletons += class.len();
continue;
}
let (occurrences, sides): (Vec<RegionOccurrence>, Vec<RegionSide>) =
class.into_iter().unzip();
let contents: Vec<FragmentFingerprint> =
occurrences.iter().map(|entry| entry.content).collect();
let clone_type = if contents.iter().all(|&content| content == contents[0]) {
CloneClass::Type1
} else {
CloneClass::Type2
};
regions.push(Confirmed {
region: StructuralRegion {
fingerprint: stable_id::clone_group_fingerprint(
variant,
clone_type,
if clone_type == CloneClass::Type1 {
&contents
} else {
std::slice::from_ref(&normalized_content)
},
),
clone_type,
statements: candidate.statements,
occurrences,
},
sides,
});
}
}
regions.sort_by(|a, b| {
a.region
.fingerprint
.cmp(&b.region.fingerprint)
.then_with(|| a.region.clone_type.name().cmp(b.region.clone_type.name()))
});
regions.dedup_by(|a, b| {
a.region.fingerprint == b.region.fingerprint && a.region.occurrences == b.region.occurrences
});
(regions, dropped)
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct Dropped {
pub(super) singletons: usize,
pub(super) overlapping: usize,
pub(super) adjoining: usize,
}
fn distinct(
class: Vec<(RegionOccurrence, RegionSide)>,
dropped: &mut Dropped,
) -> Vec<(RegionOccurrence, RegionSide)> {
let mut kept: Vec<(RegionOccurrence, RegionSide)> = Vec::with_capacity(class.len());
for entry in class {
if kept.iter().any(|(_, other)| {
other.file == entry.1.file && maximal::intersects(other.range, entry.1.range)
}) {
dropped.overlapping += 1;
continue;
}
if kept
.iter()
.any(|(_, other)| maximal::adjoins(other, &entry.1))
{
dropped.adjoining += 1;
continue;
}
kept.push(entry);
}
kept
}
pub(super) fn grow_runs(
confirmed: &mut Vec<Confirmed>,
dropped: &mut Dropped,
files: &[SyntaxIrFile],
offsets: &[usize],
variant: &BuildVariant,
literals: LiteralNorm,
) -> usize {
let candidates = merge_adjacent(confirmed);
if candidates.is_empty() {
return 0;
}
let (grown, again) = confirm_regions(&candidates, files, offsets, variant, literals);
dropped.singletons += again.singletons;
dropped.overlapping += again.overlapping;
dropped.adjoining += again.adjoining;
let before = confirmed.len();
confirmed.extend(grown);
confirmed.sort_by_key(|entry| entry.region.fingerprint);
confirmed.dedup_by(|a, b| {
a.region.fingerprint == b.region.fingerprint && a.region.occurrences == b.region.occurrences
});
confirmed.len() - before
}
pub(super) struct Confirmed {
pub(super) region: StructuralRegion,
pub(super) sides: Vec<RegionSide>,
}
pub(super) fn merge_adjacent(confirmed: &[Confirmed]) -> Vec<SharedRegion> {
let mut alignments: BTreeMap<Alignment, Vec<&Confirmed>> = BTreeMap::new();
for entry in confirmed {
let Some(alignment) = alignment_of(entry) else {
continue;
};
alignments.entry(alignment).or_default().push(entry);
}
let mut joined = Vec::new();
for mut runs in alignments.into_values() {
runs.sort_by_key(|entry| entry.sides[0].run.start);
let mut chain: Option<Chain> = None;
for run in runs {
let touches = chain
.as_ref()
.is_some_and(|grown| run.sides[0].run.start <= grown.sides[0].run.end());
match chain.as_mut() {
Some(grown) if touches => grown.absorb(&run.sides),
_ => {
if let Some(region) = chain.take().and_then(Chain::finish) {
joined.push(region);
}
chain = Some(Chain::starting_at(&run.sides));
}
}
}
if let Some(region) = chain.and_then(Chain::finish) {
joined.push(region);
}
}
joined.sort_unstable();
joined.dedup();
joined
}
struct Chain {
sides: Vec<RegionSide>,
longest: u32,
}
impl Chain {
fn starting_at(sides: &[RegionSide]) -> Self {
Self {
sides: sides.to_vec(),
longest: sides.first().map_or(0, |side| side.run.length),
}
}
fn absorb(&mut self, sides: &[RegionSide]) {
for (grown, part) in self.sides.iter_mut().zip(sides) {
grown.run.length = part.run.end().max(grown.run.end()) - grown.run.start;
grown.range.start = grown.range.start.min(part.range.start);
grown.range.end = grown.range.end.max(part.range.end);
}
self.longest = self
.longest
.max(sides.first().map_or(0, |side| side.run.length));
}
fn overlaps_itself(&self) -> bool {
self.sides.iter().enumerate().any(|(index, here)| {
self.sides[index + 1..].iter().any(|there| {
here.file == there.file && maximal::intersects(here.range, there.range)
})
})
}
fn finish(mut self) -> Option<SharedRegion> {
let statements = self.sides.first()?.run.length;
if statements <= self.longest {
return None;
}
if self.overlaps_itself() {
return None;
}
self.sides.sort_unstable();
Some(SharedRegion {
occurrences: self.sides,
statements,
})
}
}
type Alignment = Vec<(usize, usize, u32, i64)>;
fn alignment_of(entry: &Confirmed) -> Option<Alignment> {
let anchor = i64::from(entry.sides.first()?.run.start);
Some(
entry
.sides
.iter()
.map(|side| {
(
side.file,
side.unit,
side.run.block,
i64::from(side.run.start) - anchor,
)
})
.collect(),
)
}
pub(super) fn drop_subsumed(regions: &mut Vec<StructuralRegion>) -> usize {
let before = regions.len();
let mut order: Vec<usize> = (0..regions.len()).collect();
order.sort_by_key(|&index| {
(
std::cmp::Reverse(regions[index].statements),
regions[index].fingerprint,
)
});
let mut dropped = vec![false; regions.len()];
let mut coverage = RegionCoverageIndex::default();
for &inner in &order {
let covered = coverage
.candidates(®ions[inner])
.into_iter()
.any(|outer| covers_run(®ions[outer], ®ions[inner]));
if covered {
dropped[inner] = true;
} else {
coverage.insert(inner, ®ions[inner]);
}
}
*regions = std::mem::take(regions)
.into_iter()
.zip(&dropped)
.filter_map(|(region, &drop)| (!drop).then_some(region))
.collect();
before - regions.len()
}
#[derive(Default)]
struct RegionCoverageIndex {
by_file: BTreeMap<usize, BTreeMap<usize, Vec<IndexedOccurrence>>>,
}
#[derive(Clone, Copy)]
struct IndexedOccurrence {
end: usize,
region: usize,
}
impl RegionCoverageIndex {
fn insert(&mut self, region: usize, value: &StructuralRegion) {
for occurrence in &value.occurrences {
self.by_file
.entry(occurrence.file)
.or_default()
.entry(occurrence.range.start)
.or_default()
.push(IndexedOccurrence {
end: occurrence.range.end,
region,
});
}
}
fn candidates(&self, value: &StructuralRegion) -> BTreeSet<usize> {
let mut best: Option<BTreeSet<usize>> = None;
for occurrence in &value.occurrences {
let candidates = self.covering(occurrence);
if candidates.is_empty() {
return candidates;
}
if best
.as_ref()
.is_none_or(|current| candidates.len() < current.len())
{
best = Some(candidates);
}
}
best.unwrap_or_default()
}
fn covering(&self, occurrence: &RegionOccurrence) -> BTreeSet<usize> {
let Some(starts) = self.by_file.get(&occurrence.file) else {
return BTreeSet::new();
};
starts
.range(..=occurrence.range.start)
.flat_map(|(_, covers)| covers)
.filter(|cover| occurrence.range.end <= cover.end)
.map(|cover| cover.region)
.collect()
}
}
pub(super) fn covers_run(outer: &StructuralRegion, inner: &StructuralRegion) -> bool {
if outer.fingerprint == inner.fingerprint || outer.clone_type > inner.clone_type {
return false;
}
inner.occurrences.iter().all(|occurrence| {
outer.occurrences.iter().any(|cover| {
cover.file == occurrence.file
&& cover.range.start <= occurrence.range.start
&& occurrence.range.end <= cover.range.end
})
})
}
fn resolve_occurrence(
side: RegionSide,
files: &[SyntaxIrFile],
offsets: &[usize],
variant: &BuildVariant,
literals: LiteralNorm,
) -> Option<(RegionOccurrence, FragmentFingerprint)> {
let file = files.get(side.file)?;
let (start, end) = token_span(&file.tokens, side.range);
if start >= end {
return None;
}
let tokens = &file.tokens[start..end];
let context = FileContext {
frontend_version: file.frontend_version,
language: file.language,
};
let fingerprint =
|norm| stable_id::fragment_fingerprint(variant, &context, "statement-run", tokens, norm);
let lines = line_range(tokens);
Some((
RegionOccurrence {
file: side.file,
unit: offsets[side.file] + side.unit,
range: side.range,
start_line: lines.0,
end_line: lines.1,
token_start: start,
token_end: end,
content: fingerprint(ContentNorm::Raw),
},
fingerprint(ContentNorm::Normalized(literals)),
))
}
fn token_span(tokens: &[Token], range: ByteRange) -> (usize, usize) {
let start = tokens.partition_point(|token| token.span.start_byte < range.start);
let end = tokens.partition_point(|token| token.span.end_byte <= range.end);
(start, end.max(start))
}