Skip to main content

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}