use super::*;
pub(crate) const FLAT_MIN_CHILDREN: usize = 50;
pub(crate) const FLAT_MAX_EDIT: usize = 1000;
pub(crate) fn flat_children(root_id: usize, meta: &ASTMetadata) -> Option<Vec<usize>> {
let info = meta.node_info.get(&root_id)?;
if info.children.len() >= FLAT_MIN_CHILDREN {
Some(info.children.clone())
} else {
None
}
}
pub(crate) fn sole_flat_child(root_id: usize, meta: &ASTMetadata) -> Option<usize> {
let info = meta.node_info.get(&root_id)?;
let mut flat = info
.children
.iter()
.copied()
.filter(|&c| flat_children(c, meta).is_some());
let candidate = flat.next()?;
if flat.next().is_some() {
return None;
}
Some(candidate)
}
pub(crate) fn myers_lcs(a: &[u64], b: &[u64], max_edit: usize) -> Option<Vec<(usize, usize)>> {
let n = a.len();
let m = b.len();
if n == 0 || m == 0 {
return Some(vec![]);
}
let limit = max_edit.min(n + m);
let offset = limit + 1; let v_size = 2 * limit + 3;
let mut v = vec![0usize; v_size];
let mut snapshots: Vec<Vec<usize>> = Vec::with_capacity(limit + 1);
for d in 0..=limit {
snapshots.push(v.clone()); for k in (-(d as i64)..=(d as i64)).step_by(2) {
let ki = (k + offset as i64) as usize;
let x = if k == -(d as i64) {
v[ki + 1] } else if k == d as i64 || v[ki - 1] >= v[ki + 1] {
v[ki - 1] + 1 } else {
v[ki + 1] };
let mut x = x;
let mut y = (x as i64 - k) as usize;
while x < n && y < m && a[x] == b[y] {
x += 1;
y += 1;
}
v[ki] = x;
if x >= n && y >= m {
return Some(backtrack_myers(&snapshots, a, b, d, offset));
}
}
}
None
}
pub(crate) fn backtrack_myers(
snapshots: &[Vec<usize>],
a: &[u64],
b: &[u64],
d: usize,
offset: usize,
) -> Vec<(usize, usize)> {
let mut matches = Vec::new();
let mut x = a.len() as i64;
let mut y = b.len() as i64;
for step in (1..=d).rev() {
let v = &snapshots[step]; let k = x - y;
let ki = (k + offset as i64) as usize;
let prev_k = if k == -(step as i64) {
k + 1 } else if k == step as i64 || v[ki - 1] >= v[ki + 1] {
k - 1 } else {
k + 1 };
let prev_x = v[(prev_k + offset as i64) as usize] as i64;
let prev_y = prev_x - prev_k;
let x_enter = if prev_k < k { prev_x + 1 } else { prev_x };
let mut xi = x;
let mut yi = y;
while xi > x_enter {
xi -= 1;
yi -= 1;
matches.push((xi as usize, yi as usize));
}
x = prev_x;
y = prev_y;
}
while x > 0 && y > 0 {
x -= 1;
y -= 1;
matches.push((x as usize, y as usize));
}
matches.reverse();
matches
}
pub(crate) const FLAT_UNMATCHED_RECURSE_LIMIT: usize = 20;
pub(crate) const FLAT_UNMATCHED_RECURSE_MAX_TOTAL_SIZE: usize = 2000;
pub(crate) fn split_into_anchored_segments(
before_children: &[usize],
after_children: &[usize],
diff: &ASTDiff,
) -> Vec<(Vec<usize>, Vec<usize>)> {
let after_index_by_id: HashMap<usize, usize> = after_children
.iter()
.enumerate()
.map(|(index, &id)| (id, index))
.collect();
let mut segments = Vec::new();
let mut segment_start_before = 0;
let mut segment_start_after = 0;
for (before_index, &before_id) in before_children.iter().enumerate() {
let Some(&after_id) = diff.before_node_map.get(&before_id) else {
continue;
};
let Some(&after_index) = after_index_by_id.get(&after_id) else {
continue;
};
if after_index < segment_start_after {
continue;
}
segments.push((
before_children[segment_start_before..before_index]
.iter()
.copied()
.filter(|id| !diff.before_node_map.contains_key(id))
.collect(),
after_children[segment_start_after..after_index]
.iter()
.copied()
.filter(|id| !diff.after_node_map.contains_key(id))
.collect(),
));
segment_start_before = before_index + 1;
segment_start_after = after_index + 1;
}
segments.push((
before_children[segment_start_before..]
.iter()
.copied()
.filter(|id| !diff.before_node_map.contains_key(id))
.collect(),
after_children[segment_start_after..]
.iter()
.copied()
.filter(|id| !diff.after_node_map.contains_key(id))
.collect(),
));
segments
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn resolve_flat_tree_pair(
before_root: usize,
after_root: usize,
before_children: Vec<usize>,
after_children: Vec<usize>,
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
source: &'static str,
diff: &mut ASTDiff,
) {
resolve_child_sequence(
before_children,
after_children,
before_meta,
after_meta,
LeftoverPool::Flat,
source,
diff,
);
diff.add_mapping(
before_root,
after_root,
ASTMapping::matched_not_identical(ASTMappingReason::FlatSequenceDiff),
);
}
pub(crate) fn anchor_leftovers_by_member_name(
before_unmatched: Vec<usize>,
after_unmatched: Vec<usize>,
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
source: &'static str,
diff: &mut ASTDiff,
) -> (Vec<usize>, Vec<usize>) {
if before_unmatched.is_empty() || after_unmatched.is_empty() {
return (before_unmatched, after_unmatched);
}
let language = before_meta.language;
let bucket = |ids: &[usize], meta: &ASTMetadata| {
let mut groups: rustc_hash::FxHashMap<(&'static str, String), Vec<usize>> =
rustc_hash::FxHashMap::default();
for &id in ids {
if let Some(key) = nodes::member_identity_name(id, meta, &language) {
groups.entry(key).or_default().push(id);
}
}
groups
};
let before_groups = bucket(&before_unmatched, before_meta);
if before_groups.is_empty() {
return (before_unmatched, after_unmatched);
}
let after_groups = bucket(&after_unmatched, after_meta);
let preorder = |meta: &ASTMetadata, id: usize| {
meta.node_info
.get(&id)
.map(|i| i.preorder_index)
.unwrap_or(usize::MAX)
};
let mut pairs: Vec<(usize, usize)> = Vec::new();
for (key, before_ids) in &before_groups {
let Some(after_ids) = after_groups.get(key) else {
continue;
};
if before_ids.len() != after_ids.len() {
continue;
}
let mut before_ids = before_ids.clone();
let mut after_ids = after_ids.clone();
before_ids.sort_unstable_by_key(|&id| preorder(before_meta, id));
after_ids.sort_unstable_by_key(|&id| preorder(after_meta, id));
pairs.extend(before_ids.into_iter().zip(after_ids));
}
if pairs.is_empty() {
return (before_unmatched, after_unmatched);
}
pairs.sort_unstable_by_key(|&(b, _)| preorder(before_meta, b));
let cost_model = UnitCostModel::new(language);
let mut anchored_before: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
let mut anchored_after: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
for (b, a) in pairs {
resolve_forest(
vec![b],
vec![a],
before_meta,
after_meta,
&cost_model,
Algorithm::Apted,
source,
diff,
);
anchored_before.insert(b);
anchored_after.insert(a);
}
(
before_unmatched
.into_iter()
.filter(|id| !anchored_before.contains(id))
.collect(),
after_unmatched
.into_iter()
.filter(|id| !anchored_after.contains(id))
.collect(),
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LeftoverPool {
Flat,
Oversized,
}
pub(crate) fn resolve_child_sequence(
before_children: Vec<usize>,
after_children: Vec<usize>,
before_meta: &ASTMetadata,
after_meta: &ASTMetadata,
leftover_pool: LeftoverPool,
source: &'static str,
diff: &mut ASTDiff,
) {
let segments = split_into_anchored_segments(&before_children, &after_children, diff);
let mut before_unmatched: Vec<usize> = Vec::new();
let mut after_unmatched: Vec<usize> = Vec::new();
for (before_seg, after_seg) in segments {
let before_hashes: Vec<u64> = before_seg
.iter()
.map(|&id| before_meta.node_to_full_hash.get(&id).copied().unwrap_or(0))
.collect();
let after_hashes: Vec<u64> = after_seg
.iter()
.map(|&id| after_meta.node_to_full_hash.get(&id).copied().unwrap_or(0))
.collect();
match myers_lcs(&before_hashes, &after_hashes, FLAT_MAX_EDIT) {
Some(pairs) => {
let mut before_matched = vec![false; before_seg.len()];
let mut after_matched = vec![false; after_seg.len()];
for (bi, ai) in pairs {
before_matched[bi] = true;
after_matched[ai] = true;
emit_identical_subtree(
before_seg[bi],
after_seg[ai],
before_meta,
after_meta,
source,
diff,
);
}
before_unmatched.extend(
before_seg
.iter()
.enumerate()
.filter(|(i, _)| !before_matched[*i])
.map(|(_, &id)| id),
);
after_unmatched.extend(
after_seg
.iter()
.enumerate()
.filter(|(i, _)| !after_matched[*i])
.map(|(_, &id)| id),
);
}
None => {
for &id in &before_seg {
add_delete_mappings(id, before_meta, source, diff);
}
for &id in &after_seg {
add_insert_mappings(id, after_meta, source, diff);
}
}
}
}
let (before_unmatched, after_unmatched) = anchor_leftovers_by_member_name(
before_unmatched,
after_unmatched,
before_meta,
after_meta,
source,
diff,
);
let unmatched_total_size = subtree_size_sum(&before_unmatched, before_meta)
+ subtree_size_sum(&after_unmatched, after_meta);
if !before_unmatched.is_empty() && before_unmatched.len() == after_unmatched.len() {
let cost_model = UnitCostModel::new(before_meta.language);
for (b, a) in before_unmatched.into_iter().zip(after_unmatched) {
resolve_forest(
vec![b],
vec![a],
before_meta,
after_meta,
&cost_model,
Algorithm::Apted,
source,
diff,
);
}
} else if !before_unmatched.is_empty()
&& !after_unmatched.is_empty()
&& match leftover_pool {
LeftoverPool::Flat => {
before_unmatched.len() <= FLAT_UNMATCHED_RECURSE_LIMIT
&& after_unmatched.len() <= FLAT_UNMATCHED_RECURSE_LIMIT
&& unmatched_total_size <= FLAT_UNMATCHED_RECURSE_MAX_TOTAL_SIZE
}
LeftoverPool::Oversized => {
subtree_size_sum(&before_unmatched, before_meta)
* subtree_size_sum(&after_unmatched, after_meta)
<= APTED_MAX_CELLS
}
}
{
let cost_model = UnitCostModel::new(before_meta.language);
resolve_forest(
before_unmatched,
after_unmatched,
before_meta,
after_meta,
&cost_model,
Algorithm::Apted,
source,
diff,
);
} else if leftover_pool == LeftoverPool::Oversized {
resolve_unequal_segment_via_kind_only_anchors(
&before_unmatched,
&after_unmatched,
before_meta,
after_meta,
source,
diff,
);
} else {
for &id in &before_unmatched {
add_delete_mappings(id, before_meta, source, diff);
}
for &id in &after_unmatched {
add_insert_mappings(id, after_meta, source, diff);
}
}
}