1use crate::model::{CallLabel, CallNode, CallRelation, CallSiteId};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub enum DiffStatus {
5 Same,
6 Added,
7 Removed,
8 Modified,
9}
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct DiffNode {
13 pub key: String,
14 pub callsite: Option<CallSiteId>,
15 pub label: CallLabel,
16 pub relation: CallRelation,
17 pub before_label: Option<CallLabel>,
18 pub before_relation: Option<CallRelation>,
19 pub status: DiffStatus,
20 pub children: Vec<DiffNode>,
21}
22
23pub fn diff_optional(before: Option<&CallNode>, after: Option<&CallNode>) -> Option<DiffNode> {
24 match (before, after) {
25 (Some(before), Some(after)) => Some(diff_node(Some(before), Some(after))),
26 (Some(before), None) => Some(diff_node(Some(before), None)),
27 (None, Some(after)) => Some(diff_node(None, Some(after))),
28 (None, None) => None,
29 }
30}
31
32fn diff_node(before: Option<&CallNode>, after: Option<&CallNode>) -> DiffNode {
33 match (before, after) {
34 (Some(before), Some(after)) => DiffNode {
35 key: after.key.clone(),
36 callsite: after.callsite.clone(),
37 label: after.label.clone(),
38 relation: after.relation,
39 before_label: (!labels_equivalent(&before.label.default, &after.label.default)
40 || before.relation != after.relation)
41 .then(|| before.label.clone()),
42 before_relation: (before.relation != after.relation).then_some(before.relation),
43 status: if labels_equivalent(&before.label.default, &after.label.default)
44 && before.relation == after.relation
45 {
46 DiffStatus::Same
47 } else {
48 DiffStatus::Modified
49 },
50 children: diff_children(&before.children, &after.children),
51 },
52 (None, Some(after)) => mark_tree(after, DiffStatus::Added),
53 (Some(before), None) => mark_tree(before, DiffStatus::Removed),
54 (None, None) => unreachable!("diff_node requires at least one node"),
55 }
56}
57
58fn mark_tree(node: &CallNode, status: DiffStatus) -> DiffNode {
59 DiffNode {
60 key: node.key.clone(),
61 callsite: node.callsite.clone(),
62 label: node.label.clone(),
63 relation: node.relation,
64 before_label: None,
65 before_relation: None,
66 status,
67 children: node
68 .children
69 .iter()
70 .map(|child| mark_tree(child, status))
71 .collect(),
72 }
73}
74
75fn diff_children(before: &[CallNode], after: &[CallNode]) -> Vec<DiffNode> {
76 let n = before.len();
77 let m = after.len();
78 if n == m
79 && before
80 .iter()
81 .zip(after)
82 .all(|(before, after)| nodes_equivalent(before, after))
83 {
84 return before
85 .iter()
86 .zip(after)
87 .map(|(before, after)| diff_node(Some(before), Some(after)))
88 .collect();
89 }
90 let mut score = vec![vec![0usize; m + 1]; n + 1];
91
92 for i in (0..n).rev() {
93 for j in (0..m).rev() {
94 let matched = alignment_weight(&before[i], &after[j])
95 .map(|weight| score[i + 1][j + 1] + weight)
96 .unwrap_or_default();
97 score[i][j] = matched.max(score[i + 1][j]).max(score[i][j + 1]);
98 }
99 }
100
101 let mut result = Vec::new();
102 let (mut i, mut j) = (0, 0);
103 while i < n && j < m {
104 let matched =
105 alignment_weight(&before[i], &after[j]).map(|weight| score[i + 1][j + 1] + weight);
106 if matched == Some(score[i][j]) {
107 result.push(diff_node(Some(&before[i]), Some(&after[j])));
108 i += 1;
109 j += 1;
110 } else if score[i + 1][j] >= score[i][j + 1] {
111 result.push(diff_node(Some(&before[i]), None));
112 i += 1;
113 } else {
114 result.push(diff_node(None, Some(&after[j])));
115 j += 1;
116 }
117 }
118 while i < n {
119 result.push(diff_node(Some(&before[i]), None));
120 i += 1;
121 }
122 while j < m {
123 result.push(diff_node(None, Some(&after[j])));
124 j += 1;
125 }
126 result
127}
128
129fn nodes_equivalent(before: &CallNode, after: &CallNode) -> bool {
130 before.key == after.key
131 && labels_equivalent(&before.label.default, &after.label.default)
132 && before.relation == after.relation
133 && before.children.len() == after.children.len()
134 && before
135 .children
136 .iter()
137 .zip(&after.children)
138 .all(|(before, after)| nodes_equivalent(before, after))
139}
140
141fn alignment_weight(before: &CallNode, after: &CallNode) -> Option<usize> {
146 if before.key != after.key {
147 return None;
148 }
149 let mut weight = 10usize;
150 if before.callsite.is_some() && before.callsite == after.callsite {
151 weight += 1_000;
152 }
153 if before.label.default == after.label.default {
154 weight += 100;
155 } else if labels_equivalent(&before.label.default, &after.label.default) {
156 weight += 80;
157 }
158 if before.relation == after.relation {
159 weight += 5;
160 }
161 if same_shallow_children(before, after) {
162 weight += 25;
163 }
164 Some(weight)
165}
166
167fn labels_equivalent(before: &str, after: &str) -> bool {
168 before == after || normalize_anonymous_lambdas(before) == normalize_anonymous_lambdas(after)
169}
170
171fn normalize_anonymous_lambdas(label: &str) -> String {
172 let mut normalized = String::with_capacity(label.len());
173 let mut rest = label;
174 while let Some(index) = rest.find("λ#") {
175 normalized.push_str(&rest[..index + "λ#".len()]);
176 rest = &rest[index + "λ#".len()..];
177 let digits = rest.chars().take_while(char::is_ascii_digit).count();
178 rest = &rest[digits..];
179 }
180 normalized.push_str(rest);
181 normalized
182}
183
184fn same_shallow_children(before: &CallNode, after: &CallNode) -> bool {
185 before.children.len() == after.children.len()
186 && before
187 .children
188 .iter()
189 .zip(&after.children)
190 .all(|(before, after)| {
191 before.key == after.key && before.label.default == after.label.default
192 })
193}
194
195pub fn truncate_diff_tree(node: &mut DiffNode, max_depth: usize) {
199 truncate_diff_tree_at(node, max_depth, 0);
200}
201
202fn truncate_diff_tree_at(node: &mut DiffNode, max_depth: usize, depth: usize) {
203 if depth >= max_depth {
204 if node.children.iter().any(tree_has_changes) {
205 node.children = vec![DiffNode {
206 key: format!("{}#depth-limit", node.key),
207 callsite: None,
208 label: CallLabel::new("… changed below max depth"),
209 relation: CallRelation::Call,
210 before_label: None,
211 before_relation: None,
212 status: DiffStatus::Added,
213 children: Vec::new(),
214 }];
215 } else {
216 node.children.clear();
217 }
218 return;
219 }
220 for child in &mut node.children {
221 truncate_diff_tree_at(child, max_depth, depth + 1);
222 }
223}
224
225pub fn tree_has_changes(node: &DiffNode) -> bool {
226 node.status != DiffStatus::Same || node.children.iter().any(tree_has_changes)
227}
228
229pub fn collapse_unchanged_subtrees(node: &mut DiffNode) -> bool {
233 let carries_dispatch_context = subtree_has_dispatch_context(node);
234 let mut descendants_changed = false;
235 for child in &mut node.children {
236 descendants_changed |= collapse_unchanged_subtrees(child);
237 }
238 let changed = node.status != DiffStatus::Same || descendants_changed;
239 if !changed && !carries_dispatch_context {
240 node.children.clear();
241 }
242 changed
243}
244
245fn subtree_has_dispatch_context(node: &DiffNode) -> bool {
246 node.relation == CallRelation::DispatchCandidate
247 || node.children.iter().any(subtree_has_dispatch_context)
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::model::{CallLabel, CallSiteId};
254
255 fn node(key: &str, label: CallLabel, children: Vec<CallNode>) -> CallNode {
256 CallNode {
257 key: key.to_owned(),
258 callsite: None,
259 label,
260 relation: CallRelation::Call,
261 children,
262 }
263 }
264
265 fn call(key: &str, site: &str, label: &str) -> CallNode {
266 CallNode {
267 key: key.to_owned(),
268 callsite: Some(CallSiteId(site.to_owned())),
269 label: CallLabel::new(label),
270 relation: CallRelation::Call,
271 children: Vec::new(),
272 }
273 }
274
275 #[test]
276 fn aligns_by_semantic_key_and_records_a_display_change() {
277 let before = node("rust://save", CallLabel::new("save(order)"), Vec::new());
278 let after = node(
279 "rust://save",
280 CallLabel::new("save(next_order)"),
281 Vec::new(),
282 );
283
284 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
285 assert_eq!(diff.key, "rust://save");
286 assert_eq!(diff.status, DiffStatus::Modified);
287 assert_eq!(
288 diff.before_label
289 .as_ref()
290 .map(|label| label.default.as_str()),
291 Some("save(order)")
292 );
293 }
294
295 #[test]
296 fn typed_labels_do_not_create_noise_in_the_default_call_diff() {
297 let before = node(
298 "rust://save",
299 CallLabel::with_types("save(order)", "save(order: OldOrder)"),
300 Vec::new(),
301 );
302 let after = node(
303 "rust://save",
304 CallLabel::with_types("save(order)", "save(order: NewOrder)"),
305 Vec::new(),
306 );
307
308 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
309 assert_eq!(diff.status, DiffStatus::Same);
310 assert!(!tree_has_changes(&diff));
311 }
312
313 #[test]
314 fn collapses_unchanged_context_below_its_first_line() {
315 let stable = node(
316 "rust://stable",
317 CallLabel::new("stable()"),
318 vec![node("rust://deep", CallLabel::new("deep()"), Vec::new())],
319 );
320 let before = node(
321 "rust://root",
322 CallLabel::new("root()"),
323 vec![
324 stable.clone(),
325 node("rust://old", CallLabel::new("old()"), Vec::new()),
326 ],
327 );
328 let after = node(
329 "rust://root",
330 CallLabel::new("root()"),
331 vec![
332 stable,
333 node("rust://new", CallLabel::new("new()"), Vec::new()),
334 ],
335 );
336 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
337
338 assert!(collapse_unchanged_subtrees(&mut diff));
339 assert_eq!(diff.children[0].label.default, "stable()");
340 assert!(diff.children[0].children.is_empty());
341 }
342
343 #[test]
344 fn aligns_repeated_callees_by_callsite_and_label_before_target_fallback() {
345 let before = node(
346 "rust://root",
347 CallLabel::new("root()"),
348 vec![
349 call("rust://save", "save@1", "save(first)"),
350 call("rust://save", "save@2", "save(second)"),
351 ],
352 );
353 let after = node(
354 "rust://root",
355 CallLabel::new("root()"),
356 vec![
357 call("rust://save", "save@0", "save(inserted)"),
358 call("rust://save", "save@1", "save(first)"),
359 call("rust://save", "save@2", "save(second)"),
360 ],
361 );
362
363 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
364 assert_eq!(
365 diff.children
366 .iter()
367 .map(|node| (node.label.default.as_str(), node.status))
368 .collect::<Vec<_>>(),
369 vec![
370 ("save(inserted)", DiffStatus::Added),
371 ("save(first)", DiffStatus::Same),
372 ("save(second)", DiffStatus::Same),
373 ]
374 );
375 }
376
377 #[test]
378 fn preserves_a_deep_change_when_presentation_depth_is_small() {
379 let before = node(
380 "root",
381 CallLabel::new("root()"),
382 vec![node(
383 "a",
384 CallLabel::new("a()"),
385 vec![node("b", CallLabel::new("b()"), Vec::new())],
386 )],
387 );
388 let after = node(
389 "root",
390 CallLabel::new("root()"),
391 vec![node(
392 "a",
393 CallLabel::new("a()"),
394 vec![node(
395 "b",
396 CallLabel::new("b()"),
397 vec![node("new", CallLabel::new("new()"), Vec::new())],
398 )],
399 )],
400 );
401 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
402 assert!(tree_has_changes(&diff));
403 truncate_diff_tree(&mut diff, 1);
404 assert_eq!(
405 diff.children[0].children[0].label.default,
406 "… changed below max depth"
407 );
408 }
409}