use crate::intern::Token;
use crate::myers::sqrt;
pub struct TokenCounts {
in_before: Vec<u32>,
in_after: Vec<u32>,
eqlimit_before: u32,
eqlimit_after: u32,
}
impl TokenCounts {
pub fn new(before: &[Token], after: &[Token], minimal: bool) -> TokenCounts {
const MAX_EQLIMIT: u32 = 1024;
let eqlimit = |len: usize| {
if minimal { u32::MAX } else { sqrt(len).min(MAX_EQLIMIT) }
};
TokenCounts {
in_before: count_tokens(before),
in_after: count_tokens(after),
eqlimit_before: eqlimit(before.len()),
eqlimit_after: eqlimit(after.len()),
}
}
}
fn count_tokens(file: &[Token]) -> Vec<u32> {
let mut occurrences = Vec::new();
for token in file {
let bucket = token.0 as usize;
if bucket >= occurrences.len() {
occurrences.resize(bucket + 1, 0u32);
}
occurrences[bucket] += 1;
}
occurrences
}
pub fn preprocess<'a>(
counts: &TokenCounts,
before: &[Token],
after: &[Token],
removed: &'a mut [bool],
added: &'a mut [bool],
) -> (PreprocessedFile, PreprocessedFile) {
let occurrences_before = token_occurrences(before, &counts.in_after, counts.eqlimit_before);
let occurrences_after = token_occurrences(after, &counts.in_before, counts.eqlimit_after);
let file1 = PreprocessedFile::new(&occurrences_before, before, removed);
let file2 = PreprocessedFile::new(&occurrences_after, after, added);
(file1, file2)
}
fn token_occurrences(file: &[Token], other_counts: &[u32], eqlimit: u32) -> Vec<Occurrences> {
file.iter()
.map(|token| {
let occurrences = other_counts.get(token.0 as usize).copied().unwrap_or(0);
Occurrences::from_occurrences(occurrences, eqlimit)
})
.collect()
}
#[derive(Clone, Copy, Debug)]
enum Occurrences {
None,
Some,
Common,
}
impl Occurrences {
pub fn from_occurrences(occurrences: u32, eqlimit: u32) -> Occurrences {
if occurrences == 0 {
Occurrences::None
} else if occurrences >= eqlimit {
Occurrences::Common
} else {
Occurrences::Some
}
}
}
#[derive(Debug)]
pub struct PreprocessedFile {
pub indices: Vec<u32>,
pub tokens: Vec<Token>,
}
impl PreprocessedFile {
fn new(token_occurrences: &[Occurrences], tokens: &[Token], changed: &mut [bool]) -> PreprocessedFile {
let (tokens, indices) = prune_unmatched_tokens(tokens, token_occurrences, changed);
PreprocessedFile { indices, tokens }
}
}
fn prune_unmatched_tokens(
file: &[Token],
token_status: &[Occurrences],
changed: &mut [bool],
) -> (Vec<Token>, Vec<u32>) {
assert_eq!(token_status.len(), file.len());
file.iter()
.zip(token_status)
.enumerate()
.filter_map(|(i, (&token, &status))| {
let prune = match status {
Occurrences::None => true,
Occurrences::Some => false,
Occurrences::Common => should_prune_common_line(token_status, i),
};
if prune {
changed[i] = true;
None
} else {
Some((token, i as u32))
}
})
.unzip()
}
fn should_prune_common_line(token_status: &[Occurrences], pos: usize) -> bool {
const WINDOW_SIZE: usize = 100;
let mut unmatched_before = 0;
let mut common_before = 1;
let start = pos.saturating_sub(WINDOW_SIZE);
for status in token_status[start..pos].iter().rev() {
match status {
Occurrences::None => {
unmatched_before += 1;
}
Occurrences::Common => {
common_before += 1;
}
Occurrences::Some => break,
}
}
if unmatched_before == 0 {
return false;
}
let end = token_status.len().min(pos + WINDOW_SIZE + 1);
let mut unmatched_after = 0;
let mut common_after = 0;
for status in token_status[pos..end].iter() {
match status {
Occurrences::None => {
unmatched_after += 1;
}
Occurrences::Common => {
common_after += 1;
}
Occurrences::Some => break,
}
}
if unmatched_after == 0 {
return false;
}
let common = common_before + common_after;
let unmatched = unmatched_before + unmatched_after;
unmatched > 3 * common
}
#[cfg(test)]
mod tests {
use super::{Occurrences, should_prune_common_line};
#[test]
fn common_line_pruning_ignores_distant_context() {
let mut token_status = vec![Occurrences::Some; 700];
token_status[100..400].fill(Occurrences::None);
token_status[400..450].fill(Occurrences::None);
token_status[450..500].fill(Occurrences::Common);
token_status[500..550].fill(Occurrences::Common);
token_status[550..600].fill(Occurrences::None);
assert!(
!should_prune_common_line(&token_status, 500),
"only the last 100 items before the current line should contribute to the backward scan"
);
}
#[test]
fn candidate_line_counts_exactly_twice() {
let run = |before: usize, after: usize| {
let mut token_status = vec![Occurrences::Some; 40];
let pos = 20;
token_status[pos - before..pos].fill(Occurrences::None);
token_status[pos] = Occurrences::Common;
token_status[pos + 1..=pos + after].fill(Occurrences::None);
should_prune_common_line(&token_status, pos)
};
assert!(
!run(3, 3),
"six unmatched lines must keep the candidate: counting it twice gives `6 > 3 * 2`, which is false"
);
assert!(
run(3, 4),
"seven unmatched lines must prune the candidate: counting it twice gives `7 > 3 * 2`, which is true"
);
}
#[test]
fn forward_scan_reaches_a_hundred_lines_past_the_candidate() {
let pos = 120;
let mut token_status = vec![Occurrences::Some; 300];
token_status[pos - 3..pos].fill(Occurrences::None);
token_status[pos..pos + 25].fill(Occurrences::Common);
token_status[pos + 25..=pos + 100].fill(Occurrences::None);
assert!(
should_prune_common_line(&token_status, pos),
"the unmatched line 100 positions after the candidate must be counted to exceed the pruning threshold"
);
token_status[pos + 100] = Occurrences::Some;
assert!(
!should_prune_common_line(&token_status, pos),
"without the hundredth unmatched line, the candidate must stay at the threshold and be kept"
);
}
}