Skip to main content

nichlink/registry_core/mir/
merge.rs

1//! Evidence-aware merge of static MIR candidates and live call edges.
2//! 静态 MIR 候选与 live 调用边的证据归并。
3
4use crate::registry_core::declaration::{CallEdge, EvidenceKind};
5
6use super::model::{CallRelation, MirCall};
7
8/// Merge static MIR candidates with live edges into one evidence-aware
9/// relation list.
10/// 将静态 MIR 候选与 live 边归并为一份带证据等级的关系列表。
11///
12/// Live edges win over MIR candidates with the same logical symbols. A
13/// static candidate that was not observed remains visible as `Mir`, so a
14/// missing branch is not silently mistaken for a successful call.
15/// 逻辑符号相同的边以 Live 证据为准。未被观察到的静态候选仍保留为
16/// `Mir`,不会把未执行分支误报成已经成功调用。
17pub fn merge_call_relations(
18    static_calls: &[MirCall],
19    runtime_calls: &[CallEdge],
20) -> Vec<CallRelation> {
21    let mut relations = Vec::new();
22    for edge in runtime_calls {
23        relations.push(CallRelation {
24            caller: edge.caller.function.to_owned(),
25            callee: edge.callee.function.to_owned(),
26            evidence: EvidenceKind::Live,
27            source: edge.callee.source,
28            mir_line: None,
29            caller_frame: Some(edge.caller.frame_id),
30            callee_frame: Some(edge.callee.frame_id),
31        });
32    }
33    for edge in static_calls {
34        if relations.iter().any(|relation| {
35            same_symbol(&relation.caller, &edge.caller)
36                && same_symbol(&relation.callee, &edge.callee)
37        }) {
38            continue;
39        }
40        relations.push(CallRelation {
41            caller: edge.caller.clone(),
42            callee: edge.callee.clone(),
43            evidence: EvidenceKind::Mir,
44            source: None,
45            mir_line: Some(edge.mir_line),
46            caller_frame: None,
47            callee_frame: None,
48        });
49    }
50    relations
51}
52
53/// Whether two logical symbols name the same function.
54/// 两个逻辑符号是否指向同一个函数。
55///
56/// A live trace names a function by its bare symbol (`button`) while a MIR
57/// candidate carries the fully qualified path (`crate::ui::button`), so the
58/// match must also accept one side being a path suffix of the other. The suffix
59/// has to start right after a `::` boundary: a bare `ends_with` would treat
60/// `button` as a suffix of `fastbutton` and silently discard a real candidate.
61/// `strip_suffix` plus the boundary check preserves the original behaviour
62/// without allocating the two `format!("::{right}")` strings on every
63/// comparison — this runs in the nested merge loop, so those allocations were
64/// paid per relation per candidate.
65/// live trace 用裸符号(`button`)命名函数,而 MIR 候选取的是全限定路径
66/// (`crate::ui::button`),因此匹配还须接受一方是另一方路径后缀。后缀必须紧接 `::`
67/// 边界:裸的 `ends_with` 会把 `button` 当成 `fastbutton` 的后缀,静默丢掉一个真实候选。
68/// `strip_suffix` 加边界检查在保持原行为的同时,不再每次比较都分配两个
69/// `format!("::{right}")` 字符串——这段代码跑在归并的嵌套循环里,这些分配是按“每条关系
70/// × 每个候选”付出的。
71/// Pinned by `same_symbol_requires_a_path_boundary_before_the_suffix`.
72/// 由 `same_symbol_requires_a_path_boundary_before_the_suffix` 钉住。
73pub fn same_symbol(left: &str, right: &str) -> bool {
74    left == right
75        || left
76            .strip_suffix(right)
77            .is_some_and(|prefix| prefix.ends_with("::"))
78        || right
79            .strip_suffix(left)
80            .is_some_and(|prefix| prefix.ends_with("::"))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::super::model::{CallEvidence, MirCall};
86    use super::{merge_call_relations, same_symbol};
87    use crate::registry_core::declaration::{CallEdge, CallSite, EvidenceKind, SourceLocation};
88    use crate::registry_core::identity::NodeId;
89
90    fn live_edge(caller: &'static str, callee: &'static str) -> CallEdge {
91        fn site(function: &'static str, line: usize) -> CallSite {
92            CallSite {
93                node: NodeId::from_path("a.rs", "A"),
94                function,
95                frame_id: 1,
96                source: Some(SourceLocation {
97                    file: "a.rs",
98                    line: line.try_into().unwrap(),
99                    column: 1,
100                    function,
101                }),
102            }
103        }
104        CallEdge {
105            caller: site(caller, 1),
106            callee: site(callee, 2),
107        }
108    }
109
110    #[test]
111    fn live_edges_win_over_same_symbol_mir_candidates() {
112        let static_calls = vec![MirCall {
113            caller: "crate::a".to_owned(),
114            callee: "crate::b".to_owned(),
115            mir_line: 1,
116        }];
117        let relations = merge_call_relations(&static_calls, &[live_edge("a", "b")]);
118        assert_eq!(relations.len(), 1);
119        assert_eq!(relations[0].evidence, CallEvidence::Live);
120    }
121
122    #[test]
123    fn unobserved_mir_candidate_stays_visible_as_mir() {
124        let static_calls = vec![MirCall {
125            caller: "crate::a".to_owned(),
126            callee: "crate::c".to_owned(),
127            mir_line: 7,
128        }];
129        let relations = merge_call_relations(&static_calls, &[]);
130        assert_eq!(relations.len(), 1);
131        assert_eq!(relations[0].evidence, EvidenceKind::Mir);
132        assert_eq!(relations[0].mir_line, Some(7));
133        assert!(!relations[0].evidence.confirmed());
134    }
135
136    /// A suffix match is only a match when the suffix starts at a `::`
137    /// boundary: `button` must not absorb `fastbutton`.
138    /// 后缀只有在 `::` 边界处开始才算匹配:`button` 不能吞掉 `fastbutton`。
139    #[test]
140    fn same_symbol_requires_a_path_boundary_before_the_suffix() {
141        assert!(same_symbol("crate::ui::button", "button"));
142        assert!(same_symbol("button", "crate::ui::button"));
143        assert!(same_symbol("crate::ui::button", "crate::ui::button"));
144        assert!(!same_symbol("crate::fastbutton", "button"));
145        assert!(!same_symbol("fastbutton", "button"));
146        assert!(!same_symbol("crate::ui::button", "crate::other::button"));
147        // An empty side is a degenerate suffix: it only matches through `::`
148        // the same way the old `ends_with` form did.
149        // 空的一侧是退化后缀:与旧的 `ends_with` 写法一样,只有经 `::` 才匹配。
150        assert!(!same_symbol("crate::ui", ""));
151        assert!(!same_symbol("", "crate::ui"));
152        assert!(same_symbol("crate::ui::", ""));
153    }
154}