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 let mut score = vec![vec![0usize; m + 1]; n + 1];
79
80 for i in (0..n).rev() {
81 for j in (0..m).rev() {
82 let matched = alignment_weight(&before[i], &after[j])
83 .map(|weight| score[i + 1][j + 1] + weight)
84 .unwrap_or_default();
85 score[i][j] = matched.max(score[i + 1][j]).max(score[i][j + 1]);
86 }
87 }
88
89 let mut result = Vec::new();
90 let (mut i, mut j) = (0, 0);
91 while i < n && j < m {
92 let matched =
93 alignment_weight(&before[i], &after[j]).map(|weight| score[i + 1][j + 1] + weight);
94 if matched == Some(score[i][j]) {
95 result.push(diff_node(Some(&before[i]), Some(&after[j])));
96 i += 1;
97 j += 1;
98 } else if score[i + 1][j] >= score[i][j + 1] {
99 result.push(diff_node(Some(&before[i]), None));
100 i += 1;
101 } else {
102 result.push(diff_node(None, Some(&after[j])));
103 j += 1;
104 }
105 }
106 while i < n {
107 result.push(diff_node(Some(&before[i]), None));
108 i += 1;
109 }
110 while j < m {
111 result.push(diff_node(None, Some(&after[j])));
112 j += 1;
113 }
114 result
115}
116
117fn alignment_weight(before: &CallNode, after: &CallNode) -> Option<usize> {
122 if before.key != after.key {
123 return None;
124 }
125 let mut weight = 10usize;
126 if before.callsite.is_some() && before.callsite == after.callsite {
127 weight += 1_000;
128 }
129 if before.label.default == after.label.default {
130 weight += 100;
131 } else if labels_equivalent(&before.label.default, &after.label.default) {
132 weight += 80;
133 }
134 if before.relation == after.relation {
135 weight += 5;
136 }
137 if same_shallow_children(before, after) {
138 weight += 25;
139 }
140 Some(weight)
141}
142
143fn labels_equivalent(before: &str, after: &str) -> bool {
144 before == after || normalize_anonymous_lambdas(before) == normalize_anonymous_lambdas(after)
145}
146
147fn normalize_anonymous_lambdas(label: &str) -> String {
148 let mut normalized = String::with_capacity(label.len());
149 let mut rest = label;
150 while let Some(index) = rest.find("λ#") {
151 normalized.push_str(&rest[..index + "λ#".len()]);
152 rest = &rest[index + "λ#".len()..];
153 let digits = rest.chars().take_while(char::is_ascii_digit).count();
154 rest = &rest[digits..];
155 }
156 normalized.push_str(rest);
157 normalized
158}
159
160fn same_shallow_children(before: &CallNode, after: &CallNode) -> bool {
161 before.children.len() == after.children.len()
162 && before
163 .children
164 .iter()
165 .zip(&after.children)
166 .all(|(before, after)| {
167 before.key == after.key && before.label.default == after.label.default
168 })
169}
170
171pub fn truncate_diff_tree(node: &mut DiffNode, max_depth: usize) {
175 truncate_diff_tree_at(node, max_depth, 0);
176}
177
178fn truncate_diff_tree_at(node: &mut DiffNode, max_depth: usize, depth: usize) {
179 if depth >= max_depth {
180 if node.children.iter().any(tree_has_changes) {
181 node.children = vec![DiffNode {
182 key: format!("{}#depth-limit", node.key),
183 callsite: None,
184 label: CallLabel::new("… changed below max depth"),
185 relation: CallRelation::Call,
186 before_label: None,
187 before_relation: None,
188 status: DiffStatus::Added,
189 children: Vec::new(),
190 }];
191 } else {
192 node.children.clear();
193 }
194 return;
195 }
196 for child in &mut node.children {
197 truncate_diff_tree_at(child, max_depth, depth + 1);
198 }
199}
200
201pub fn tree_has_changes(node: &DiffNode) -> bool {
202 node.status != DiffStatus::Same || node.children.iter().any(tree_has_changes)
203}
204
205pub fn collapse_unchanged_subtrees(node: &mut DiffNode) -> bool {
209 let carries_dispatch_context = subtree_has_dispatch_context(node);
210 let mut descendants_changed = false;
211 for child in &mut node.children {
212 descendants_changed |= collapse_unchanged_subtrees(child);
213 }
214 let changed = node.status != DiffStatus::Same || descendants_changed;
215 if !changed && !carries_dispatch_context {
216 node.children.clear();
217 }
218 changed
219}
220
221fn subtree_has_dispatch_context(node: &DiffNode) -> bool {
222 node.relation == CallRelation::DispatchCandidate
223 || node.children.iter().any(subtree_has_dispatch_context)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::model::{CallLabel, CallSiteId};
230
231 fn node(key: &str, label: CallLabel, children: Vec<CallNode>) -> CallNode {
232 CallNode {
233 key: key.to_owned(),
234 callsite: None,
235 label,
236 relation: CallRelation::Call,
237 children,
238 }
239 }
240
241 fn call(key: &str, site: &str, label: &str) -> CallNode {
242 CallNode {
243 key: key.to_owned(),
244 callsite: Some(CallSiteId(site.to_owned())),
245 label: CallLabel::new(label),
246 relation: CallRelation::Call,
247 children: Vec::new(),
248 }
249 }
250
251 #[test]
252 fn aligns_by_semantic_key_and_records_a_display_change() {
253 let before = node("rust://save", CallLabel::new("save(order)"), Vec::new());
254 let after = node(
255 "rust://save",
256 CallLabel::new("save(next_order)"),
257 Vec::new(),
258 );
259
260 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
261 assert_eq!(diff.key, "rust://save");
262 assert_eq!(diff.status, DiffStatus::Modified);
263 assert_eq!(
264 diff.before_label
265 .as_ref()
266 .map(|label| label.default.as_str()),
267 Some("save(order)")
268 );
269 }
270
271 #[test]
272 fn typed_labels_do_not_create_noise_in_the_default_call_diff() {
273 let before = node(
274 "rust://save",
275 CallLabel::with_types("save(order)", "save(order: OldOrder)"),
276 Vec::new(),
277 );
278 let after = node(
279 "rust://save",
280 CallLabel::with_types("save(order)", "save(order: NewOrder)"),
281 Vec::new(),
282 );
283
284 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
285 assert_eq!(diff.status, DiffStatus::Same);
286 assert!(!tree_has_changes(&diff));
287 }
288
289 #[test]
290 fn collapses_unchanged_context_below_its_first_line() {
291 let stable = node(
292 "rust://stable",
293 CallLabel::new("stable()"),
294 vec![node("rust://deep", CallLabel::new("deep()"), Vec::new())],
295 );
296 let before = node(
297 "rust://root",
298 CallLabel::new("root()"),
299 vec![
300 stable.clone(),
301 node("rust://old", CallLabel::new("old()"), Vec::new()),
302 ],
303 );
304 let after = node(
305 "rust://root",
306 CallLabel::new("root()"),
307 vec![
308 stable,
309 node("rust://new", CallLabel::new("new()"), Vec::new()),
310 ],
311 );
312 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
313
314 assert!(collapse_unchanged_subtrees(&mut diff));
315 assert_eq!(diff.children[0].label.default, "stable()");
316 assert!(diff.children[0].children.is_empty());
317 }
318
319 #[test]
320 fn aligns_repeated_callees_by_callsite_and_label_before_target_fallback() {
321 let before = node(
322 "rust://root",
323 CallLabel::new("root()"),
324 vec![
325 call("rust://save", "save@1", "save(first)"),
326 call("rust://save", "save@2", "save(second)"),
327 ],
328 );
329 let after = node(
330 "rust://root",
331 CallLabel::new("root()"),
332 vec![
333 call("rust://save", "save@0", "save(inserted)"),
334 call("rust://save", "save@1", "save(first)"),
335 call("rust://save", "save@2", "save(second)"),
336 ],
337 );
338
339 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
340 assert_eq!(
341 diff.children
342 .iter()
343 .map(|node| (node.label.default.as_str(), node.status))
344 .collect::<Vec<_>>(),
345 vec![
346 ("save(inserted)", DiffStatus::Added),
347 ("save(first)", DiffStatus::Same),
348 ("save(second)", DiffStatus::Same),
349 ]
350 );
351 }
352
353 #[test]
354 fn preserves_a_deep_change_when_presentation_depth_is_small() {
355 let before = node(
356 "root",
357 CallLabel::new("root()"),
358 vec![node(
359 "a",
360 CallLabel::new("a()"),
361 vec![node("b", CallLabel::new("b()"), Vec::new())],
362 )],
363 );
364 let after = node(
365 "root",
366 CallLabel::new("root()"),
367 vec![node(
368 "a",
369 CallLabel::new("a()"),
370 vec![node(
371 "b",
372 CallLabel::new("b()"),
373 vec![node("new", CallLabel::new("new()"), Vec::new())],
374 )],
375 )],
376 );
377 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
378 assert!(tree_has_changes(&diff));
379 truncate_diff_tree(&mut diff, 1);
380 assert_eq!(
381 diff.children[0].children[0].label.default,
382 "… changed below max depth"
383 );
384 }
385}