rapx/analysis/alias/mfp/interproc.rs
1/// Interprocedural analysis utilities
2use rustc_middle::mir::{Body, TerminatorKind};
3use rustc_mir_dataflow::ResultsCursor;
4use std::collections::HashSet;
5
6use super::super::{AliasPair, FnAliasPairs};
7use super::intraproc::{FnAliasAnalyzer, PlaceId};
8
9/// Extract root local and field path from a PlaceId
10/// Returns (root_local, field_path)
11fn extract_fields(place: &PlaceId) -> (usize, Vec<usize>) {
12 let mut fields = Vec::new();
13 let mut current = place;
14
15 // Traverse from leaf to root, collecting field indices
16 loop {
17 match current {
18 PlaceId::Local(idx) => return (*idx, fields),
19 PlaceId::Field { base, field_idx } => {
20 fields.push(*field_idx);
21 current = base;
22 }
23 }
24 }
25}
26
27/// Extract only the field path from a PlaceId (excluding the root local)
28/// Returns field indices in order from root to leaf
29/// Example: _15.0.1 returns [0, 1]
30fn extract_field_path(place: &PlaceId) -> Vec<usize> {
31 let mut fields = Vec::new();
32 let mut current = place;
33
34 // Traverse from leaf to root
35 loop {
36 match current {
37 PlaceId::Local(_) => {
38 fields.reverse(); // Reverse to get root-to-leaf order
39 return fields;
40 }
41 PlaceId::Field { base, field_idx } => {
42 fields.push(*field_idx);
43 current = base;
44 }
45 }
46 }
47}
48
49/// Check if one field path is a proper prefix of another
50/// Returns true if `prefix` is a prefix of `full`
51///
52/// Examples:
53/// - is_field_prefix([], [0]) = true (parent-child)
54/// - is_field_prefix([0], [0, 1]) = true (parent-child)
55/// - is_field_prefix([0], [1]) = false (siblings, not prefix)
56/// - is_field_prefix([0, 1], [0, 2]) = false (different fields at same level)
57fn is_field_prefix(prefix: &[usize], full: &[usize]) -> bool {
58 if prefix.len() > full.len() {
59 return false;
60 }
61 prefix == &full[..prefix.len()]
62}
63
64/// Extract function summary from analysis results
65///
66/// This function uses transitive closure to identify all aliases related to
67/// function parameters and return values, including those connected through
68/// temporary variables.
69pub fn extract_summary<'tcx>(
70 results: &mut ResultsCursor<'_, 'tcx, FnAliasAnalyzer<'tcx>>,
71 body: &Body<'tcx>,
72) -> FnAliasPairs {
73 let arg_count = body.arg_count;
74 let mut summary = FnAliasPairs::new(arg_count);
75
76 // Find all Return terminators and extract aliases at those points
77 for (block_id, block_data) in body.basic_blocks.iter_enumerated() {
78 if let Some(terminator) = &block_data.terminator {
79 if matches!(terminator.kind, TerminatorKind::Return) {
80 // Seek to the end of this block (before the terminator)
81 results.seek_to_block_end(block_id);
82
83 let state = results.get();
84 let analyzer = results.analysis();
85 let place_info = analyzer.place_info();
86
87 // Step 1: Collect all alias pairs at this return point
88 // We need to examine all aliases, not just those directly involving args/return
89 let mut all_pairs = Vec::new();
90 for (idx_i, idx_j) in state.get_all_alias_pairs() {
91 if let (Some(place_i), Some(place_j)) =
92 (place_info.get_place(idx_i), place_info.get_place(idx_j))
93 {
94 all_pairs.push((idx_i, idx_j, place_i, place_j));
95 }
96 }
97
98 // Step 2: Initialize relevant_places with all places whose root is a parameter or return value
99 // Index 0 is return value, indices 1..=arg_count are arguments
100 let mut relevant_places = HashSet::new();
101 for idx in 0..place_info.num_places() {
102 if let Some(place) = place_info.get_place(idx) {
103 if place.root_local() <= arg_count {
104 relevant_places.insert(idx);
105 }
106 }
107 }
108
109 // Step 3: Expand relevant_places using transitive closure
110 // If a place aliases to a relevant place, it becomes relevant too
111 // This captures aliases that flow through temporary variables
112 // Example: _0 aliases _2, and _2 aliases _1.0, then _2 is relevant
113 const MAX_ITERATIONS: usize = 10;
114 for _iteration in 0..MAX_ITERATIONS {
115 let mut changed = false;
116 for &(idx_i, idx_j, _, _) in &all_pairs {
117 // If one place is relevant and the other isn't, make the other relevant
118 if relevant_places.contains(&idx_i) && !relevant_places.contains(&idx_j) {
119 relevant_places.insert(idx_j);
120 changed = true;
121 }
122 if relevant_places.contains(&idx_j) && !relevant_places.contains(&idx_i) {
123 relevant_places.insert(idx_i);
124 changed = true;
125 }
126 }
127 // Converged when no more places become relevant
128 if !changed {
129 break;
130 }
131 }
132
133 // Step 4: Collect candidate aliases from two sources
134 //
135 // We collect all potential aliases between parameters and return values into
136 // a candidate set, which will be filtered and compressed in Step 5.
137 //
138 // Two sources of candidates:
139 // 4.1: Aliases derived through bridge variables (indirect connections)
140 // 4.2: Aliases directly present in all_pairs (direct connections)
141 //
142 let mut candidate_aliases = std::collections::HashSet::new();
143
144 // Step 4.1: Derive aliases through bridge variables
145 //
146 // Problem: When analyzing complex nested structures (e.g., Vec::from_raw_parts_in),
147 // we may have aliases like:
148 // - _0.0.0.0 ≈ _15 (deeply nested field aliases with temporary variable)
149 // - _1 ≈ _15.0 (parameter aliases with field of temporary variable)
150 //
151 // Both sides are connected through the bridge variable _15, but:
152 // 1. _15 is a temporary (root > arg_count), so it won't appear in final summary
153 // 2. There's no direct alias between _0.x and _1 in all_pairs
154 // 3. Union-Find guarantees transitivity within all_pairs, but the connection
155 // happens at different structural levels (parent vs child fields)
156 //
157 // Solution: For alias pairs connected through the same bridge variable where one
158 // side references the bridge's parent and the other references a child field,
159 // we derive a direct alias between the parameter/return places, compressing
160 // deep field paths to maintain precision while avoiding temporary variables.
161 //
162 // Example derivation:
163 // Input: _0.0.0.0 ≈ _15 (place_i ≈ place_j)
164 // _1 ≈ _15.0 (place_k ≈ place_m)
165 // Check: place_j.root (15) == place_m.root (15) ✓ (same bridge)
166 // _15 is prefix of _15.0 ✓ (parent-child relation)
167 // Output: _0.0 ≈ _1 (compress _0's deep fields to first level)
168 //
169 for &(idx_i, idx_j, place_i, place_j) in &all_pairs {
170 if !relevant_places.contains(&idx_i) || !relevant_places.contains(&idx_j) {
171 continue;
172 }
173
174 for &(idx_k, idx_m, place_k, place_m) in &all_pairs {
175 if !relevant_places.contains(&idx_k) || !relevant_places.contains(&idx_m) {
176 continue;
177 }
178
179 // Check if both alias pairs share the same bridge variable (by root)
180 if place_j.root_local() != place_m.root_local() {
181 continue;
182 }
183
184 // Extract field paths for prefix checking
185 let j_fields = extract_field_path(place_j);
186 let m_fields = extract_field_path(place_m);
187
188 // Verify parent-child relationship (proper prefix, not sibling fields)
189 // E.g., [] is prefix of [0], but [0] is NOT prefix of [1]
190 if !is_field_prefix(&j_fields, &m_fields)
191 && !is_field_prefix(&m_fields, &j_fields)
192 {
193 continue;
194 }
195
196 // Extract roots and fields from both sides
197 let (root_i, mut fields_i) = extract_fields(place_i);
198 let (root_k, fields_k) = extract_fields(place_k);
199
200 // Only derive aliases between parameters and return value
201 if root_i > arg_count || root_k > arg_count {
202 continue;
203 }
204
205 // Compress deep field paths to first level only
206 // This balances precision (distinguishing struct fields) with
207 // generality (avoiding over-specification)
208 fields_i.reverse(); // extract_fields returns reversed order
209 if fields_i.len() > 1 {
210 fields_i = vec![fields_i[0]];
211 }
212
213 let mut fields_k_reversed = fields_k.clone();
214 fields_k_reversed.reverse();
215
216 // Create the derived alias and add to candidates
217 let mut alias = AliasPair::new(root_i, root_k);
218 alias.lhs_fields = fields_i;
219 alias.rhs_fields = fields_k_reversed;
220
221 candidate_aliases.insert(alias);
222 }
223 }
224
225 // Step 4.2: Direct aliases from all_pairs
226 //
227 // Collect aliases that are directly present between parameters and return values
228 // in the Union-Find analysis results. These may overlap with derived aliases from
229 // Step 4.1, but duplicates are automatically handled by the HashSet.
230 //
231 for (idx_i, idx_j, place_i, place_j) in all_pairs {
232 if relevant_places.contains(&idx_i) && relevant_places.contains(&idx_j) {
233 let (root_i, mut fields_i) = extract_fields(place_i);
234 let (root_j, mut fields_j) = extract_fields(place_j);
235
236 // Only include if both roots are parameters or return value
237 if root_i <= arg_count && root_j <= arg_count {
238 // Fields were collected from leaf to root, reverse them
239 fields_i.reverse();
240 fields_j.reverse();
241
242 // Create field-sensitive AliasPair and add to candidates
243 let mut alias = AliasPair::new(root_i, root_j);
244 alias.lhs_fields = fields_i;
245 alias.rhs_fields = fields_j;
246 candidate_aliases.insert(alias);
247 }
248 }
249 }
250
251 // Step 5: Normalize and filter redundant aliases
252 //
253 // Step 5.1: Normalize alias order for consistent comparison
254 //
255 // Alias relationships are symmetric: (A, B) ≡ (B, A)
256 // However, without normalization, (3, 0.0.0) and (0.0, 3) would be
257 // treated as having different local pairs and cannot be compared for
258 // subsumption relationships.
259 //
260 // We normalize by ensuring left_local <= right_local, which allows
261 // the filter to recognize that (0.0, 3) subsumes (0.0.0, 3) even if
262 // the latter was originally collected as (3, 0.0.0).
263 //
264 let normalized_aliases: std::collections::HashSet<_> = candidate_aliases
265 .iter()
266 .map(|alias| {
267 let mut normalized = alias.clone();
268 if normalized.left_local > normalized.right_local {
269 normalized.swap(); // Swap both locals and fields
270 }
271 normalized
272 })
273 .collect();
274
275 // Step 5.2: Filter redundant aliases
276 //
277 // The normalized candidate set may contain redundant aliases such as:
278 // - Self-aliases: (0, 0), (1, 1)
279 // - Prefix-subsumed aliases: (0.0, 1) subsumes (0.0.0, 1), (0.0.0.0, 1)
280 // - Reversed but equivalent: (3, 0.0.0) and (0.0, 3) after normalization
281 //
282 // We filter these to produce a minimal, canonical summary that retains
283 // precision while avoiding over-specification.
284 //
285 let filtered_aliases = filter_redundant_aliases(normalized_aliases);
286 for alias in filtered_aliases.clone() {
287 summary.add_alias(alias);
288 }
289 }
290 }
291 }
292
293 summary
294}
295
296/// Filter redundant aliases from a candidate set
297///
298/// This function removes several types of redundant aliases to produce a minimal,
299/// canonical summary:
300///
301/// 1. **Self-aliases**: Aliases where both sides refer to the same place
302/// Example: (0, 0), (1, 1)
303///
304/// 2. **Prefix-subsumed aliases**: When one alias is strictly more general than another
305///
306/// Examples of redundancy patterns eliminated:
307///
308/// a) Single-side prefix (left more specific):
309/// - (1.0, 2) subsumes (1.0.0.0, 2) → keep (1.0, 2), remove (1.0.0.0, 2)
310/// - Rationale: If field 1.0 aliases with 2, then 1.0.0.0 (a sub-field) also aliases
311///
312/// b) Single-side prefix (right more specific):
313/// - (1.0, 2) subsumes (1.0, 2.0) → keep (1.0, 2), remove (1.0, 2.0)
314/// - Rationale: If 1.0 aliases with field 2, then 1.0 aliases with 2.0 (a sub-field)
315///
316/// c) Double-side prefix (synchronized fields):
317/// - (0, 1) subsumes (0.1, 1.1) → keep (0, 1), remove (0.1, 1.1)
318/// - Rationale: If 0 and 1 alias, their corresponding fields also alias
319/// - This handles cases like struct field synchronization
320///
321/// The filtering strategy:
322/// - For each pair of aliases with the same (left_local, right_local):
323/// - Check bidirectional subsumption (a subsumes b, or b subsumes a)
324/// - Keep the more general alias (shorter total field depth)
325/// - This ensures order-independence and catches all redundancy patterns
326///
327/// Returns: A filtered HashSet containing only non-redundant aliases
328fn filter_redundant_aliases(
329 aliases: std::collections::HashSet<AliasPair>,
330) -> std::collections::HashSet<AliasPair> {
331 use std::collections::HashSet;
332
333 let aliases_vec: Vec<_> = aliases.iter().cloned().collect();
334 let mut to_remove = HashSet::new();
335
336 for i in 0..aliases_vec.len() {
337 let alias_a = &aliases_vec[i];
338
339 // Skip if already marked for removal
340 if to_remove.contains(alias_a) {
341 continue;
342 }
343
344 // Rule 1: Remove self-aliases (same local)
345 // Example: (0, 0), (1, 1)
346 if alias_a.left_local == alias_a.right_local {
347 to_remove.insert(alias_a.clone());
348 continue;
349 }
350
351 // Rule 2: Check for prefix subsumption with other aliases
352 for j in 0..aliases_vec.len() {
353 if i == j {
354 continue;
355 }
356 let alias_b = &aliases_vec[j];
357
358 // Skip if already marked for removal
359 if to_remove.contains(alias_b) {
360 continue;
361 }
362
363 // Only compare aliases with the same locals
364 // Note: After normalization, this ensures left_local <= right_local for both,
365 // so (3, 0.0.0) normalized to (0.0.0, 3) can be compared with (0.0, 3)
366 if alias_a.left_local != alias_b.left_local
367 || alias_a.right_local != alias_b.right_local
368 {
369 continue;
370 }
371
372 // Check bidirectional subsumption relationships
373 //
374 // Subsumption means one alias is more general (has shorter field paths)
375 // than another. We check both directions:
376 // - Does a subsume b? (a is more general)
377 // - Does b subsume a? (b is more general)
378
379 // Check if a's fields subsume b's fields
380 let lhs_a_subsumes_b = is_strict_prefix(&alias_a.lhs_fields, &alias_b.lhs_fields)
381 || alias_a.lhs_fields == alias_b.lhs_fields;
382 let rhs_a_subsumes_b = is_strict_prefix(&alias_a.rhs_fields, &alias_b.rhs_fields)
383 || alias_a.rhs_fields == alias_b.rhs_fields;
384
385 // Check if b's fields subsume a's fields
386 let lhs_b_subsumes_a = is_strict_prefix(&alias_b.lhs_fields, &alias_a.lhs_fields)
387 || alias_b.lhs_fields == alias_a.lhs_fields;
388 let rhs_b_subsumes_a = is_strict_prefix(&alias_b.rhs_fields, &alias_a.rhs_fields)
389 || alias_b.rhs_fields == alias_a.rhs_fields;
390
391 // Determine if there's a subsumption relationship
392 let lhs_a_strict = is_strict_prefix(&alias_a.lhs_fields, &alias_b.lhs_fields);
393 let rhs_a_strict = is_strict_prefix(&alias_a.rhs_fields, &alias_b.rhs_fields);
394 let lhs_b_strict = is_strict_prefix(&alias_b.lhs_fields, &alias_a.lhs_fields);
395 let rhs_b_strict = is_strict_prefix(&alias_b.rhs_fields, &alias_a.rhs_fields);
396
397 // a subsumes b if both sides subsume and at least one is strict
398 let a_subsumes_b =
399 lhs_a_subsumes_b && rhs_a_subsumes_b && (lhs_a_strict || rhs_a_strict);
400 // b subsumes a if both sides subsume and at least one is strict
401 let b_subsumes_a =
402 lhs_b_subsumes_a && rhs_b_subsumes_a && (lhs_b_strict || rhs_b_strict);
403
404 // If there's a subsumption relationship, mark the more specific one for removal
405 if a_subsumes_b || b_subsumes_a {
406 // Compare specificity: more general alias has lower total field depth
407 let spec_a = alias_specificity(alias_a);
408 let spec_b = alias_specificity(alias_b);
409
410 if spec_a < spec_b {
411 // a is more general, mark b for removal
412 to_remove.insert(alias_b.clone());
413 } else if spec_b < spec_a {
414 // b is more general, mark a for removal
415 to_remove.insert(alias_a.clone());
416 break; // Stop comparing alias_a since it's marked for removal
417 }
418 // If equal specificity, keep both (shouldn't happen with strict prefix)
419 }
420 }
421 }
422
423 // Return the result with marked aliases removed
424 aliases.difference(&to_remove).cloned().collect()
425}
426
427/// Calculate the specificity of an alias based on total field depth
428///
429/// Specificity is measured as the sum of field depths on both sides.
430/// Lower values indicate more general (less specific) aliases.
431///
432/// Examples:
433/// - (0, 1): specificity = 0 + 0 = 0 (most general)
434/// - (0.1, 1): specificity = 1 + 0 = 1
435/// - (0.1, 1.1): specificity = 1 + 1 = 2
436/// - (1.0.0.0, 2): specificity = 3 + 0 = 3
437///
438/// When two aliases have a subsumption relationship, we keep the one
439/// with lower specificity (more general).
440fn alias_specificity(alias: &AliasPair) -> usize {
441 alias.lhs_fields.len() + alias.rhs_fields.len()
442}
443
444/// Check if `prefix` is a strict prefix of `full`
445///
446/// A strict prefix means:
447/// - `prefix` is shorter than `full`
448/// - All elements of `prefix` match the corresponding elements in `full`
449///
450/// Examples:
451/// - is_strict_prefix([0], [0, 1]) = true
452/// - is_strict_prefix([], [0]) = true
453/// - is_strict_prefix([0], [0]) = false (equal, not strict)
454/// - is_strict_prefix([0], [1]) = false (not a prefix)
455fn is_strict_prefix(prefix: &[usize], full: &[usize]) -> bool {
456 prefix.len() < full.len() && prefix == &full[..prefix.len()]
457}