use crate::frontend::{LiteralKind, Token, TokenKind};
pub const SUBSTITUTION_VERSION: &str = "substitution-v1";
const ALIGNMENT_LIMIT: usize = 1 << 20;
const WIDTHS: [&str; 5] = ["8", "16", "32", "64", "128"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
pub from: String,
pub to: String,
pub literal: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Witness {
pub changes: Vec<Change>,
pub edits: usize,
}
impl Witness {
#[must_use]
pub fn one_width_apart(&self) -> Option<(&'static str, &'static str)> {
if self.changes.is_empty() {
return None;
}
WIDTHS
.into_iter()
.flat_map(|from| WIDTHS.into_iter().map(move |to| (from, to)))
.find(|&(from, to)| {
from != to
&& self.changes.iter().all(|change| {
change.from.contains(from) && change.from.replace(from, to) == change.to
})
})
}
#[must_use]
pub fn written_once_per_width(&self) -> bool {
self.one_width_apart().is_some() && !self.touches_a_literal()
}
#[must_use]
pub fn touches_a_literal(&self) -> bool {
self.changes.iter().any(|change| change.literal)
}
}
#[must_use]
pub fn witness(left: &[Token], right: &[Token]) -> Option<Witness> {
let (rows, columns) = (left.len(), right.len());
if rows.checked_mul(columns)? > ALIGNMENT_LIMIT {
return None;
}
let stride = columns + 1;
let mut score = vec![0u32; (rows + 1) * stride];
for i in (0..rows).rev() {
for j in (0..columns).rev() {
let paired = pairing(&left[i], &right[j]);
let best = score[(i + 1) * stride + j].max(score[i * stride + j + 1]);
score[i * stride + j] = if paired > 0 {
best.max(score[(i + 1) * stride + j + 1] + paired)
} else {
best
};
}
}
let mut changes: Vec<Change> = Vec::new();
let mut edits = 0usize;
let (mut i, mut j) = (0usize, 0usize);
while i < rows && j < columns {
let paired = pairing(&left[i], &right[j]);
let here = score[i * stride + j];
if paired > 0 && here == score[(i + 1) * stride + j + 1] + paired {
note(&mut changes, &left[i], &right[j]);
i += 1;
j += 1;
} else if here == score[(i + 1) * stride + j] {
i += 1;
edits += 1;
} else {
j += 1;
edits += 1;
}
}
edits += (rows - i) + (columns - j);
Some(Witness { changes, edits })
}
fn pairing(left: &Token, right: &Token) -> u32 {
if left.kind.tag() != right.kind.tag() {
0
} else if left.text == right.text {
3
} else {
2
}
}
fn note(changes: &mut Vec<Change>, from: &Token, to: &Token) {
if from.text == to.text {
return;
}
let change = Change {
from: from.text.to_string(),
to: to.text.to_string(),
literal: is_literal(from.kind) || is_literal(to.kind),
};
if !changes.contains(&change) {
changes.push(change);
}
}
const fn is_literal(kind: TokenKind) -> bool {
matches!(
kind,
TokenKind::Literal(
LiteralKind::Integer
| LiteralKind::Float
| LiteralKind::String
| LiteralKind::Char
| LiteralKind::Bool
)
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::frontend::SourceSpan;
fn tokens(spec: &[(TokenKind, &str)]) -> Vec<Token> {
spec.iter()
.map(|&(kind, text)| Token {
kind,
text: text.into(),
span: SourceSpan {
start_byte: 0,
end_byte: text.len(),
start_line: 1,
start_column: 1,
},
})
.collect()
}
fn name(text: &str) -> (TokenKind, &str) {
(TokenKind::Identifier, text)
}
fn number(text: &str) -> (TokenKind, &str) {
(TokenKind::Literal(LiteralKind::Integer), text)
}
#[test]
fn a_token_with_no_counterpart_is_an_edit_and_not_a_change() {
let left = tokens(&[name("a")]);
let right = tokens(&[name("a"), name("b")]);
let witness = witness(&left, &right).unwrap();
assert!(witness.changes.is_empty(), "nothing was substituted");
assert_eq!(witness.edits, 1);
}
#[test]
fn a_pair_too_large_to_align_has_no_witness() {
let long = tokens(&vec![name("a"); 1100]);
assert_eq!(witness(&long, &long), None);
}
#[test]
fn identical_runs_witness_no_change() {
let left = tokens(&[name("a"), name("b")]);
let witness = witness(&left, &left).unwrap();
assert!(witness.changes.is_empty());
assert_eq!(witness.edits, 0);
assert_eq!(witness.one_width_apart(), None);
}
#[test]
fn two_runs_of_one_shape_are_read_as_substitutions_throughout() {
let left = tokens(&[name("b"), name("x")]);
let right = tokens(&[name("y"), name("b")]);
let witness = witness(&left, &right).unwrap();
assert_eq!(witness.edits, 0);
assert_eq!(witness.changes.len(), 2);
}
#[test]
fn an_identical_name_settles_which_of_two_alignments_is_read() {
let left = tokens(&[name("a"), name("b"), name("c")]);
let right = tokens(&[name("a"), name("c")]);
let witness = witness(&left, &right).unwrap();
assert!(witness.changes.is_empty());
assert_eq!(witness.edits, 1);
}
#[test]
fn a_width_swap_survives_an_edit_beside_it() {
let left = tokens(&[name("U32"), name("read32")]);
let right = tokens(&[name("U64"), name("read64"), name("finalize")]);
let witness = witness(&left, &right).unwrap();
assert_eq!(witness.one_width_apart(), Some(("32", "64")));
assert_eq!(witness.edits, 1);
}
#[test]
fn a_kind_that_differs_is_not_paired_off() {
let left = tokens(&[name("value")]);
let right = tokens(&[number("7")]);
let witness = witness(&left, &right).unwrap();
assert!(witness.changes.is_empty(), "a name did not become a number");
assert_eq!(witness.edits, 2);
}
#[test]
fn a_change_is_recorded_once_however_often_it_recurs() {
let left = tokens(&[name("u32"), name("u32"), name("u32")]);
let right = tokens(&[name("u64"), name("u64"), name("u64")]);
let witness = witness(&left, &right).unwrap();
assert_eq!(witness.changes.len(), 1);
}
#[test]
fn one_width_everywhere_is_recognised() {
let left = tokens(&[name("U32"), name("XXH_read32"), name("XXH_swap32")]);
let right = tokens(&[name("U64"), name("XXH_read64"), name("XXH_swap64")]);
assert_eq!(
witness(&left, &right).unwrap().one_width_apart(),
Some(("32", "64"))
);
}
#[test]
fn a_rename_that_is_not_a_width_is_not_one() {
let left = tokens(&[name("LZ4_isLittleEndian")]);
let right = tokens(&[name("XXH_isLittleEndian")]);
assert_eq!(witness(&left, &right).unwrap().one_width_apart(), None);
}
#[test]
fn a_width_beside_a_rename_that_is_not_one_is_not_one_either() {
let left = tokens(&[name("long"), name("m8Index")]);
let right = tokens(&[name("short"), name("m4Index")]);
assert_eq!(witness(&left, &right).unwrap().one_width_apart(), None);
}
#[test]
fn a_changed_constant_is_visible_as_one() {
let left = tokens(&[name("wait"), number("24")]);
let right = tokens(&[name("wait"), number("1")]);
assert!(witness(&left, &right).unwrap().touches_a_literal());
}
#[test]
fn a_constant_that_reads_like_a_width_is_still_a_constant() {
let left = tokens(&[number("32")]);
let right = tokens(&[number("64")]);
let witness = witness(&left, &right).unwrap();
assert_eq!(witness.one_width_apart(), Some(("32", "64")));
assert!(witness.touches_a_literal());
}
}