1use std::borrow::Cow;
2
3use crate::model::{CallLabel, CallNode, CallRelation, CallSiteId};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum DiffStatus {
7 Same,
8 Added,
9 Removed,
10 Modified,
11}
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct DiffNode {
15 pub key: String,
16 pub callsite: Option<CallSiteId>,
17 pub label: CallLabel,
18 pub relation: CallRelation,
19 pub before_label: Option<CallLabel>,
20 pub before_relation: Option<CallRelation>,
21 pub status: DiffStatus,
22 pub children: Vec<DiffNode>,
23}
24
25pub fn diff_optional(before: Option<&CallNode>, after: Option<&CallNode>) -> Option<DiffNode> {
26 match (before, after) {
27 (Some(before), Some(after)) => Some(diff_node(Some(before), Some(after))),
28 (Some(before), None) => Some(diff_node(Some(before), None)),
29 (None, Some(after)) => Some(diff_node(None, Some(after))),
30 (None, None) => None,
31 }
32}
33
34fn diff_node(before: Option<&CallNode>, after: Option<&CallNode>) -> DiffNode {
35 match (before, after) {
36 (Some(before), Some(after)) => DiffNode {
37 key: after.key.clone(),
38 callsite: after.callsite.clone(),
39 label: after.label.clone(),
40 relation: after.relation,
41 before_label: (!labels_equivalent(&before.label.default, &after.label.default)
42 || before.relation != after.relation)
43 .then(|| before.label.clone()),
44 before_relation: (before.relation != after.relation).then_some(before.relation),
45 status: if labels_equivalent(&before.label.default, &after.label.default)
46 && before.relation == after.relation
47 {
48 DiffStatus::Same
49 } else {
50 DiffStatus::Modified
51 },
52 children: diff_children(&before.children, &after.children),
53 },
54 (None, Some(after)) => mark_tree(after, DiffStatus::Added),
55 (Some(before), None) => mark_tree(before, DiffStatus::Removed),
56 (None, None) => unreachable!("diff_node requires at least one node"),
57 }
58}
59
60fn mark_tree(node: &CallNode, status: DiffStatus) -> DiffNode {
61 DiffNode {
62 key: node.key.clone(),
63 callsite: node.callsite.clone(),
64 label: node.label.clone(),
65 relation: node.relation,
66 before_label: None,
67 before_relation: None,
68 status,
69 children: node
70 .children
71 .iter()
72 .map(|child| mark_tree(child, status))
73 .collect(),
74 }
75}
76
77fn diff_children(before: &[CallNode], after: &[CallNode]) -> Vec<DiffNode> {
78 let n = before.len();
79 let m = after.len();
80 if n == m
81 && before
82 .iter()
83 .zip(after)
84 .all(|(before, after)| nodes_equivalent(before, after))
85 {
86 return before
87 .iter()
88 .zip(after)
89 .map(|(before, after)| diff_node(Some(before), Some(after)))
90 .collect();
91 }
92 let before_alignment = before.iter().map(AlignmentNode::new).collect::<Vec<_>>();
93 let after_alignment = after.iter().map(AlignmentNode::new).collect::<Vec<_>>();
94 let width = m + 1;
95 let mut score = vec![0usize; (n + 1).saturating_mul(width)];
96
97 for i in (0..n).rev() {
98 for j in (0..m).rev() {
99 let matched = alignment_weight(&before_alignment[i], &after_alignment[j])
100 .map(|weight| score[(i + 1) * width + j + 1] + weight)
101 .unwrap_or_default();
102 score[i * width + j] = matched
103 .max(score[(i + 1) * width + j])
104 .max(score[i * width + j + 1]);
105 }
106 }
107
108 let mut result = Vec::new();
109 let (mut i, mut j) = (0, 0);
110 while i < n && j < m {
111 let matched = alignment_weight(&before_alignment[i], &after_alignment[j])
112 .map(|weight| score[(i + 1) * width + j + 1] + weight);
113 if matched == Some(score[i * width + j]) {
114 result.push(diff_node(Some(&before[i]), Some(&after[j])));
115 i += 1;
116 j += 1;
117 } else if score[(i + 1) * width + j] >= score[i * width + j + 1] {
118 result.push(diff_node(Some(&before[i]), None));
119 i += 1;
120 } else {
121 result.push(diff_node(None, Some(&after[j])));
122 j += 1;
123 }
124 }
125 while i < n {
126 result.push(diff_node(Some(&before[i]), None));
127 i += 1;
128 }
129 while j < m {
130 result.push(diff_node(None, Some(&after[j])));
131 j += 1;
132 }
133 result
134}
135
136struct AlignmentNode<'a> {
137 node: &'a CallNode,
138 key: Cow<'a, str>,
139 label: Cow<'a, str>,
140}
141
142impl<'a> AlignmentNode<'a> {
143 fn new(node: &'a CallNode) -> Self {
144 Self {
145 node,
146 key: normalize_anonymous_lambda_keys(&node.key),
147 label: normalize_anonymous_lambdas(&node.label.default),
148 }
149 }
150}
151
152pub(crate) fn nodes_equivalent(before: &CallNode, after: &CallNode) -> bool {
153 semantic_keys_equivalent(&before.key, &after.key)
154 && labels_equivalent(&before.label.default, &after.label.default)
155 && before.relation == after.relation
156 && before.children.len() == after.children.len()
157 && before
158 .children
159 .iter()
160 .zip(&after.children)
161 .all(|(before, after)| nodes_equivalent(before, after))
162}
163
164fn alignment_weight(before: &AlignmentNode<'_>, after: &AlignmentNode<'_>) -> Option<usize> {
170 if before.key != after.key {
171 return None;
172 }
173 let mut weight = 10usize;
174 if before.node.label.default == after.node.label.default
175 && !before.node.label.default.contains("λ#")
176 {
177 weight += 100;
178 } else if before.label == after.label {
179 weight += 80;
180 }
181 if before.node.relation == after.node.relation {
182 weight += 5;
183 }
184 if same_shallow_children(before.node, after.node) {
185 weight += 25;
186 }
187 weight += matching_descendant_weight(before.node, after.node, 2).min(100);
188 Some(weight)
189}
190
191fn semantic_keys_equivalent(before: &str, after: &str) -> bool {
192 before == after
193 || normalize_anonymous_lambda_keys(before) == normalize_anonymous_lambda_keys(after)
194}
195
196fn normalize_anonymous_lambda_keys(key: &str) -> Cow<'_, str> {
197 if !key.contains("{lambda:") {
198 return Cow::Borrowed(key);
199 }
200 let mut normalized = String::with_capacity(key.len());
201 let mut remainder = key;
202 while let Some(start) = remainder.find("{lambda:") {
203 normalized.push_str(&remainder[..start]);
204 normalized.push_str("{lambda}");
205 let located = &remainder[start..];
206 let Some(end) = located.find('}') else {
207 normalized.push_str(located);
208 return Cow::Owned(normalized);
209 };
210 remainder = &located[end + 1..];
211 }
212 normalized.push_str(remainder);
213 Cow::Owned(normalized)
214}
215
216fn labels_equivalent(before: &str, after: &str) -> bool {
217 before == after || normalize_anonymous_lambdas(before) == normalize_anonymous_lambdas(after)
218}
219
220fn normalize_anonymous_lambdas(label: &str) -> Cow<'_, str> {
221 if !label.contains("λ#") {
222 return Cow::Borrowed(label);
223 }
224 let mut normalized = String::with_capacity(label.len());
225 let mut rest = label;
226 while let Some(index) = rest.find("λ#") {
227 normalized.push_str(&rest[..index + "λ#".len()]);
228 rest = &rest[index + "λ#".len()..];
229 let digits = rest.chars().take_while(char::is_ascii_digit).count();
230 rest = &rest[digits..];
231 }
232 normalized.push_str(rest);
233 Cow::Owned(normalized)
234}
235
236fn same_shallow_children(before: &CallNode, after: &CallNode) -> bool {
237 before.children.len() == after.children.len()
238 && before
239 .children
240 .iter()
241 .zip(&after.children)
242 .all(|(before, after)| {
243 semantic_keys_equivalent(&before.key, &after.key)
244 && labels_equivalent(&before.label.default, &after.label.default)
245 })
246}
247
248fn matching_descendant_weight(before: &CallNode, after: &CallNode, depth: usize) -> usize {
249 if depth == 0 || before.children.len() != after.children.len() {
250 return 0;
251 }
252 before
253 .children
254 .iter()
255 .zip(&after.children)
256 .map(|(before, after)| {
257 if !semantic_keys_equivalent(&before.key, &after.key) {
258 return 0;
259 }
260 10 + usize::from(labels_equivalent(
261 &before.label.default,
262 &after.label.default,
263 )) * 10
264 + matching_descendant_weight(before, after, depth - 1)
265 })
266 .sum()
267}
268
269pub fn truncate_diff_tree(node: &mut DiffNode, max_depth: usize) {
273 truncate_diff_tree_at(node, max_depth, 0);
274}
275
276fn truncate_diff_tree_at(node: &mut DiffNode, max_depth: usize, depth: usize) {
277 if depth >= max_depth {
278 if node.children.iter().any(tree_has_changes) {
279 node.children = vec![DiffNode {
280 key: format!("{}#depth-limit", node.key),
281 callsite: None,
282 label: CallLabel::new("… changed below max depth"),
283 relation: CallRelation::Call,
284 before_label: None,
285 before_relation: None,
286 status: DiffStatus::Added,
287 children: Vec::new(),
288 }];
289 } else {
290 node.children.clear();
291 }
292 return;
293 }
294 for child in &mut node.children {
295 truncate_diff_tree_at(child, max_depth, depth + 1);
296 }
297}
298
299pub fn tree_has_changes(node: &DiffNode) -> bool {
300 node.status != DiffStatus::Same || node.children.iter().any(tree_has_changes)
301}
302
303pub fn collapse_unchanged_subtrees(node: &mut DiffNode) -> bool {
307 let carries_dispatch_context = subtree_has_dispatch_context(node);
308 let mut descendants_changed = false;
309 for child in &mut node.children {
310 descendants_changed |= collapse_unchanged_subtrees(child);
311 }
312 let changed = node.status != DiffStatus::Same || descendants_changed;
313 if !changed && !carries_dispatch_context {
314 node.children.clear();
315 }
316 changed
317}
318
319fn subtree_has_dispatch_context(node: &DiffNode) -> bool {
320 node.relation == CallRelation::DispatchCandidate
321 || node.children.iter().any(subtree_has_dispatch_context)
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::model::{CallLabel, CallSiteId};
328
329 fn node(key: &str, label: CallLabel, children: Vec<CallNode>) -> CallNode {
330 CallNode {
331 key: key.to_owned(),
332 callsite: None,
333 label,
334 relation: CallRelation::Call,
335 children,
336 }
337 }
338
339 fn call(key: &str, site: &str, label: &str) -> CallNode {
340 CallNode {
341 key: key.to_owned(),
342 callsite: Some(CallSiteId(site.to_owned())),
343 label: CallLabel::new(label),
344 relation: CallRelation::Call,
345 children: Vec::new(),
346 }
347 }
348
349 #[test]
350 fn aligns_by_semantic_key_and_records_a_display_change() {
351 let before = node("rust://save", CallLabel::new("save(order)"), Vec::new());
352 let after = node(
353 "rust://save",
354 CallLabel::new("save(next_order)"),
355 Vec::new(),
356 );
357
358 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
359 assert_eq!(diff.key, "rust://save");
360 assert_eq!(diff.status, DiffStatus::Modified);
361 assert_eq!(
362 diff.before_label
363 .as_ref()
364 .map(|label| label.default.as_str()),
365 Some("save(order)")
366 );
367 }
368
369 #[test]
370 fn typed_labels_do_not_create_noise_in_the_default_call_diff() {
371 let before = node(
372 "rust://save",
373 CallLabel::with_types("save(order)", "save(order: OldOrder)"),
374 Vec::new(),
375 );
376 let after = node(
377 "rust://save",
378 CallLabel::with_types("save(order)", "save(order: NewOrder)"),
379 Vec::new(),
380 );
381
382 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
383 assert_eq!(diff.status, DiffStatus::Same);
384 assert!(!tree_has_changes(&diff));
385 }
386
387 #[test]
388 fn collapses_unchanged_context_below_its_first_line() {
389 let stable = node(
390 "rust://stable",
391 CallLabel::new("stable()"),
392 vec![node("rust://deep", CallLabel::new("deep()"), Vec::new())],
393 );
394 let before = node(
395 "rust://root",
396 CallLabel::new("root()"),
397 vec![
398 stable.clone(),
399 node("rust://old", CallLabel::new("old()"), Vec::new()),
400 ],
401 );
402 let after = node(
403 "rust://root",
404 CallLabel::new("root()"),
405 vec![
406 stable,
407 node("rust://new", CallLabel::new("new()"), Vec::new()),
408 ],
409 );
410 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
411
412 assert!(collapse_unchanged_subtrees(&mut diff));
413 assert_eq!(diff.children[0].label.default, "stable()");
414 assert!(diff.children[0].children.is_empty());
415 }
416
417 #[test]
418 fn aligns_repeated_callees_by_callsite_and_label_before_target_fallback() {
419 let before = node(
420 "rust://root",
421 CallLabel::new("root()"),
422 vec![
423 call("rust://save", "save@1", "save(first)"),
424 call("rust://save", "save@2", "save(second)"),
425 ],
426 );
427 let after = node(
428 "rust://root",
429 CallLabel::new("root()"),
430 vec![
431 call("rust://save", "save@0", "save(inserted)"),
432 call("rust://save", "save@1", "save(first)"),
433 call("rust://save", "save@2", "save(second)"),
434 ],
435 );
436
437 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
438 assert_eq!(
439 diff.children
440 .iter()
441 .map(|node| (node.label.default.as_str(), node.status))
442 .collect::<Vec<_>>(),
443 vec![
444 ("save(inserted)", DiffStatus::Added),
445 ("save(first)", DiffStatus::Same),
446 ("save(second)", DiffStatus::Same),
447 ]
448 );
449 }
450
451 #[test]
452 fn absolute_source_line_shifts_do_not_corrupt_repeated_call_alignment() {
453 let before = node(
454 "rust://root",
455 CallLabel::new("root()"),
456 vec![
457 call("rust://touch", "touch@4", "touch(item)"),
458 call("rust://touch", "touch@5", "touch(item)"),
459 call("rust://touch", "touch@6", "touch(item)"),
460 ],
461 );
462 let after = node(
463 "rust://root",
464 CallLabel::new("root()"),
465 vec![
466 call("rust://touch", "touch@5", "touch(item)"),
467 call("rust://touch", "touch@6", "touch(item)"),
468 call("rust://touch", "touch@7", "touch(item)"),
469 call("rust://finish", "finish@8", "finish()"),
470 ],
471 );
472
473 let diff = diff_optional(Some(&before), Some(&after)).unwrap();
474 assert_eq!(diff.children.len(), 4);
475 assert!(
476 diff.children[..3]
477 .iter()
478 .all(|node| node.status == DiffStatus::Same)
479 );
480 assert_eq!(diff.children[3].label.default, "finish()");
481 assert_eq!(diff.children[3].status, DiffStatus::Added);
482 }
483
484 #[test]
485 fn preserves_a_deep_change_when_presentation_depth_is_small() {
486 let before = node(
487 "root",
488 CallLabel::new("root()"),
489 vec![node(
490 "a",
491 CallLabel::new("a()"),
492 vec![node("b", CallLabel::new("b()"), Vec::new())],
493 )],
494 );
495 let after = node(
496 "root",
497 CallLabel::new("root()"),
498 vec![node(
499 "a",
500 CallLabel::new("a()"),
501 vec![node(
502 "b",
503 CallLabel::new("b()"),
504 vec![node("new", CallLabel::new("new()"), Vec::new())],
505 )],
506 )],
507 );
508 let mut diff = diff_optional(Some(&before), Some(&after)).unwrap();
509 assert!(tree_has_changes(&diff));
510 truncate_diff_tree(&mut diff, 1);
511 assert_eq!(
512 diff.children[0].children[0].label.default,
513 "… changed below max depth"
514 );
515 }
516}