Skip to main content

rapx/analysis/points_to/
graph.rs

1use std::collections::VecDeque;
2
3use crate::compat::{FxHashMap, FxHashSet};
4use crate::helpers::def_use::{PlaceBaseKey, PlaceKey};
5
6use super::slot::{AbstractLoc, Slot};
7
8use crate::analysis::alias::default::types::ValueKind;
9use crate::limit::MAX_VALUES_PER_PATH;
10
11/// Unified points-to and value-flow graph.
12///
13/// Maintains two directed relationship types between slots:
14///
15/// * **points_to**: the slot holds a pointer/reference *into* another slot.
16///   Created by `&_x`, `&raw _x`, etc.
17/// * **value_flow**: the slot's *value* is a copy of another slot's value.
18///   Created by `_a = _b` (Copy/Move), `_a = _b as *const T` (Cast), etc.
19///
20/// Alias queries (`may_alias`) combine:
21/// * **Alias partition**: value-equivalence through assignments (union-find)
22/// * **Points-to intersection**: pointer-level aliasing through references
23#[derive(Clone, Debug)]
24pub struct PtsGraph {
25    points_to: Vec<FxHashSet<AbstractLoc>>,
26    value_flow: Vec<FxHashSet<usize>>,
27    slots: Vec<Slot>,
28    slot_index: FxHashMap<Slot, usize>,
29    may_drop: Vec<bool>,
30    need_drop: Vec<bool>,
31    /// Type classification per slot (RawPtr, Ref, Adt, etc.).
32    slot_kind: Vec<ValueKind>,
33
34    /// Alias partition: which slots are value-equivalent (union-find).
35    /// `alias_parent[i]` is the representative of i's partition,
36    /// or `i` itself if i is the root. `None` means uninitialized (singleton).
37    alias_parent: Vec<usize>,
38
39    /// `(holder, root)`: `holder` may hold the value of the partition under
40    /// `root`, as a call's return may be any one of several arguments.
41    may_hold: FxHashSet<(usize, usize)>,
42}
43
44impl PtsGraph {
45    pub fn new() -> Self {
46        PtsGraph {
47            points_to: Vec::new(),
48            value_flow: Vec::new(),
49            slots: Vec::new(),
50            slot_index: FxHashMap::default(),
51            may_drop: Vec::new(),
52            need_drop: Vec::new(),
53            slot_kind: Vec::new(),
54            alias_parent: Vec::new(),
55            may_hold: FxHashSet::default(),
56        }
57    }
58
59    pub fn slot_count(&self) -> usize {
60        self.slots.len()
61    }
62
63    pub fn get_slot(&self, idx: usize) -> Option<&Slot> {
64        self.slots.get(idx)
65    }
66
67    pub fn get_slot_idx(&self, slot: &Slot) -> Option<usize> {
68        self.slot_index.get(slot).copied()
69    }
70
71    pub fn may_drop(&self, idx: usize) -> bool {
72        self.may_drop.get(idx).copied().unwrap_or(false)
73    }
74
75    pub fn need_drop(&self, idx: usize) -> bool {
76        self.need_drop.get(idx).copied().unwrap_or(false)
77    }
78
79    // ── Slot registration ──────────────────────────────────────────
80
81    pub fn ensure_slot(&mut self, slot: Slot, may_drop: bool, need_drop: bool) -> usize {
82        if let Some(&idx) = self.slot_index.get(&slot) {
83            return idx;
84        }
85        if self.slots.len() >= MAX_VALUES_PER_PATH {
86            return 0;
87        }
88        let idx = self.slots.len();
89        self.slots.push(slot.clone());
90        self.slot_index.insert(slot, idx);
91        self.points_to.push(FxHashSet::default());
92        self.value_flow.push(FxHashSet::default());
93        self.may_drop.push(may_drop);
94        self.need_drop.push(need_drop);
95        self.slot_kind.push(ValueKind::Adt);
96        self.alias_parent.push(idx); // singleton: points to itself
97        idx
98    }
99
100    pub fn set_slot_kind(&mut self, idx: usize, kind: ValueKind) {
101        if idx < self.slot_kind.len() {
102            self.slot_kind[idx] = kind;
103        }
104    }
105
106    pub fn slot_kind(&self, idx: usize) -> ValueKind {
107        self.slot_kind.get(idx).copied().unwrap_or(ValueKind::Adt)
108    }
109
110    pub fn slot_is_ptr(&self, idx: usize) -> bool {
111        matches!(self.slot_kind(idx), ValueKind::RawPtr | ValueKind::Ref)
112    }
113
114    pub fn slot_is_ref_count(&self, idx: usize) -> bool {
115        matches!(self.slot_kind(idx), ValueKind::SpecialPtr)
116    }
117
118    // ── Value-flow updates ─────────────────────────────────────────
119
120    /// Return the direct pointee targets for a slot (non-transitive).
121    pub fn direct_pointees(&self, idx: usize) -> impl Iterator<Item = &AbstractLoc> {
122        self.points_to[idx].iter()
123    }
124
125    /// Record that `dest` points to `target`.
126    /// Strong update: clears old points-to info for `dest`.
127    pub fn assign_pointee(&mut self, dest_idx: usize, target: AbstractLoc) {
128        self.points_to[dest_idx].clear();
129        self.points_to[dest_idx].insert(target);
130    }
131
132    /// Record that `dest` has the same VALUE as `src` (Copy/Move/Cast).
133    /// This is a strong update:
134    /// - Remove `dest` from its old alias partition (other members stay)
135    /// - Put `dest` into `src`'s alias partition
136    /// - Also propagate to field slots.
137    pub fn assign_value(&mut self, dest_idx: usize, src_idx: usize) {
138        self.value_flow[dest_idx].clear();
139        self.value_flow[dest_idx].insert(src_idx);
140
141        // ── Alias partition: strong update ──
142        self.alias_move_to_partition(dest_idx, src_idx);
143
144        // ── Field-level propagation ──
145        if dest_idx < self.slots.len() && src_idx < self.slots.len() {
146            let dest_slot = self.slots[dest_idx].clone();
147            let src_slot = self.slots[src_idx].clone();
148
149            // Propagate to sub-fields: for every slot that extends dest
150            // (same local, additional field projections), find the
151            // corresponding slot that extends src and connect them.
152            let dest_prefix = &dest_slot.fields;
153            let mut field_pairs: Vec<(usize, usize)> = Vec::new();
154            for (cand, cand_s) in self.slots.iter().enumerate() {
155                if cand_s.local != dest_slot.local {
156                    continue;
157                }
158                if cand_s.fields.len() <= dest_prefix.len() {
159                    continue;
160                }
161                if cand_s.fields[..dest_prefix.len()] != *dest_prefix {
162                    continue;
163                }
164                // cand_s is a sub-field of dest (e.g., dest=_0.0, cand=_0.0.0)
165                let suffix = &cand_s.fields[dest_prefix.len()..];
166                let mut src_sub_slot = Slot::new(src_slot.local);
167                src_sub_slot.fields = src_slot.fields.clone();
168                src_sub_slot.fields.extend_from_slice(suffix);
169                if let Some(&src_sub_idx) = self.slot_index.get(&src_sub_slot) {
170                    field_pairs.push((cand, src_sub_idx));
171                }
172            }
173            for (dest_cand, src_field_idx) in field_pairs {
174                self.value_flow[dest_cand].clear();
175                self.value_flow[dest_cand].insert(src_field_idx);
176                self.alias_move_to_partition(dest_cand, src_field_idx);
177            }
178        }
179    }
180
181    /// Merge equivalence: the two slots may hold the same pointer.
182    /// Both inherit the union of each other's points-to set.
183    /// This is used for inter-procedural aliasing and branch join points.
184    /// Also propagates to father slots so SafeDrop can detect aliasing
185    /// through the base local (e.g. `_v.0` alias `ptr` → `_v` alias `s`).
186    pub fn merge_equivalence(&mut self, a_idx: usize, b_idx: usize) {
187        if a_idx == b_idx {
188            return;
189        }
190        // Merge points-to sets
191        let a_pts: Vec<_> = self.points_to[a_idx].iter().cloned().collect();
192        for loc in a_pts {
193            self.points_to[b_idx].insert(loc);
194        }
195        let b_pts: Vec<_> = self.points_to[b_idx].iter().cloned().collect();
196        for loc in b_pts {
197            self.points_to[a_idx].insert(loc);
198        }
199
200        // Merge alias partitions
201        self.alias_union(a_idx, b_idx);
202
203        // Propagate one level upward so SafeDrop's value-level queries
204        // can find field-level aliases (e.g. _b2.0 aliases p → _b2 aliases p).
205        self.propagate_to_father(a_idx, b_idx);
206    }
207
208    fn propagate_to_father(&mut self, a_idx: usize, b_idx: usize) {
209        let fa = self.father_of(a_idx);
210        let fb = self.father_of(b_idx);
211        let ra = fa.unwrap_or(a_idx);
212        let rb = fb.unwrap_or(b_idx);
213        if self.alias_find(ra) != self.alias_find(rb) {
214            self.alias_union(ra, rb);
215        }
216    }
217
218    fn father_of(&self, idx: usize) -> Option<usize> {
219        let slot = &self.slots[idx];
220        if slot.fields.is_empty() {
221            return None;
222        }
223        let father_slot = Slot {
224            local: slot.local,
225            fields: slot.fields[..slot.fields.len() - 1].to_vec(),
226        };
227        self.slot_index.get(&father_slot).copied()
228    }
229
230    /// `holder` may hold the value of `held`. Unlike `merge_equivalence`,
231    /// this does not make `held` alias anything else `holder` may hold.
232    pub fn hold(&mut self, holder: usize, held: usize) {
233        if holder == held {
234            return;
235        }
236        let held_pts: Vec<_> = self.points_to[held].iter().cloned().collect();
237        self.points_to[holder].extend(held_pts);
238        self.may_hold.insert((holder, self.alias_find(held)));
239        let father_holder = self.father_of(holder).unwrap_or(holder);
240        let father_held = self.alias_find(self.father_of(held).unwrap_or(held));
241        self.may_hold.insert((father_holder, father_held));
242    }
243
244    /// Conservative merge for unknown-function calls: all pointer-typed
245    /// args may alias each other and the return value.
246    pub fn conservative_call_merge(&mut self, arg_slots: &[usize]) {
247        let mut pointer_args: Vec<usize> = Vec::new();
248        for &idx in arg_slots {
249            if !self.points_to[idx].is_empty() {
250                pointer_args.push(idx);
251            } else if self.may_drop(idx) {
252                pointer_args.push(idx);
253            }
254        }
255        for i in 0..pointer_args.len() {
256            for j in (i + 1)..pointer_args.len() {
257                self.merge_equivalence(pointer_args[i], pointer_args[j]);
258            }
259        }
260    }
261
262    // ── Queries ────────────────────────────────────────────────────
263
264    /// Transitive points-to set: follow value_flow + points_to until
265    /// fixpoint.  Returns all AbstractLoc reachable from `start_idx`.
266    pub fn pts(&self, start_idx: usize) -> FxHashSet<AbstractLoc> {
267        let mut result = FxHashSet::default();
268        let mut visited = FxHashSet::default();
269        let mut queue = VecDeque::new();
270        queue.push_back(Start::Pointee(start_idx));
271        visited.insert(Visit::Pointee(start_idx));
272
273        while let Some(current) = queue.pop_front() {
274            match current {
275                Start::Pointee(idx) => {
276                    for loc in &self.points_to[idx] {
277                        if !matches!(loc, AbstractLoc::Null) {
278                            result.insert(loc.clone());
279                        }
280                    }
281                    for &src in &self.value_flow[idx] {
282                        if visited.insert(Visit::Pointee(src)) {
283                            queue.push_back(Start::Pointee(src));
284                        }
285                    }
286                }
287            }
288        }
289        result
290    }
291
292    /// May-alias check: do the pointed-to memories of `a` and `b` overlap?
293    /// Combines:
294    /// 1. Alias partition check (value-equivalence via assignments)
295    /// 2. Points-to intersection (pointer-level aliasing)
296    pub fn may_alias(&self, a_idx: usize, b_idx: usize) -> bool {
297        // Check alias partition (value-equivalence)
298        if self.alias_find(a_idx) == self.alias_find(b_idx) {
299            return true;
300        }
301        if !self.may_hold.is_empty() {
302            let held_a = self.held_partitions(a_idx);
303            if self
304                .held_partitions(b_idx)
305                .iter()
306                .any(|root| held_a.contains(root))
307            {
308                return true;
309            }
310        }
311        // Check points-to intersection
312        let pta = self.pts(a_idx);
313        if pta.is_empty() {
314            return false;
315        }
316        let ptb = self.pts(b_idx);
317        pta.intersection(&ptb).next().is_some()
318    }
319
320    // ── Inter-procedural ───────────────────────────────────────────
321
322    /// Apply callee's FnAliasPairs to the graph at a call site.
323    /// `callee_arg_slots`: [ret_dest_idx, arg₀_idx, arg₁_idx, ...]
324    pub fn apply_callee_summary(
325        &mut self,
326        callee_pairs: &crate::analysis::alias::FnAliasPairs,
327        callee_arg_slots: &[usize],
328    ) {
329        let ret_sources: FxHashSet<usize> = callee_pairs
330            .aliases()
331            .iter()
332            .filter(|alias| alias.left_local() == 0)
333            .map(|alias| alias.right_local())
334            .collect();
335        for alias in callee_pairs.aliases() {
336            let left_idx = alias.left_local();
337            let right_idx = alias.right_local();
338
339            if left_idx >= callee_arg_slots.len() || right_idx >= callee_arg_slots.len() {
340                continue;
341            }
342
343            let mut lv = callee_arg_slots[left_idx];
344            let mut rv = callee_arg_slots[right_idx];
345
346            for &field_idx in alias.lhs_fields() {
347                let field_slot = self.slots[lv].project(field_idx);
348                if let Some(idx) = self.slot_index.get(&field_slot) {
349                    lv = *idx;
350                } else {
351                    let idx = self.ensure_slot(field_slot, self.may_drop[lv], self.need_drop[lv]);
352                    lv = idx;
353                }
354            }
355            for &field_idx in alias.rhs_fields() {
356                let field_slot = self.slots[rv].project(field_idx);
357                if let Some(idx) = self.slot_index.get(&field_slot) {
358                    rv = *idx;
359                } else {
360                    let idx = self.ensure_slot(field_slot, self.may_drop[rv], self.need_drop[rv]);
361                    rv = idx;
362                }
363            }
364
365            if !self.may_drop(lv) || !self.may_drop(rv) {
366                continue;
367            }
368            if left_idx == 0 && ret_sources.len() > 1 {
369                self.hold(lv, rv);
370            } else {
371                self.merge_equivalence(lv, rv);
372            }
373        }
374    }
375
376    // ── FnAliasPairs extraction ────────────────────────────────────
377
378    /// Compute field-sensitive alias pairs among args (1..=arg_count) + return
379    /// value (0).  For each pair, checks `may_alias()` and if true, emits an
380    /// `AliasPair` with the truncated single-level field paths.
381    pub fn fn_alias_pairs(&self, arg_count: usize) -> crate::analysis::alias::FnAliasPairs {
382        let mut pairs = crate::analysis::alias::FnAliasPairs::new(arg_count);
383
384        let local_ids: Vec<usize> = (0..=arg_count).collect();
385
386        // Map each local -> its base slot index (the slot with empty fields).
387        let mut local_to_base_slot: FxHashMap<usize, usize> = FxHashMap::default();
388        for (slot_idx, s) in self.slots.iter().enumerate() {
389            if s.fields.is_empty() && s.local <= arg_count {
390                local_to_base_slot.entry(s.local).or_insert(slot_idx);
391            }
392        }
393
394        // Base-level alias check.
395        for i in 0..local_ids.len() {
396            for j in (i + 1)..local_ids.len() {
397                let li = local_ids[i];
398                let lj = local_ids[j];
399                let Some(&slot_i) = local_to_base_slot.get(&li) else {
400                    continue;
401                };
402                let Some(&slot_j) = local_to_base_slot.get(&lj) else {
403                    continue;
404                };
405                if self.may_alias(slot_i, slot_j) {
406                    let mut pair = crate::analysis::alias::AliasPair::new(li, lj);
407                    pair.lhs_fields = vec![];
408                    pair.rhs_fields = vec![];
409                    pairs.add_alias(pair);
410                }
411            }
412        }
413
414        // Field-level alias checks.
415        let field_slots: Vec<(usize, Vec<usize>)> = self
416            .slots
417            .iter()
418            .enumerate()
419            .filter_map(|(idx, slot)| {
420                if !slot.fields.is_empty() && slot.local <= arg_count {
421                    Some((idx, slot.fields.clone()))
422                } else {
423                    None
424                }
425            })
426            .collect();
427
428        for (idx_a, fields_a) in &field_slots {
429            let slot_a = &self.slots[*idx_a];
430            // Field ↔ Field
431            for (idx_b, fields_b) in &field_slots {
432                if idx_a == idx_b {
433                    continue;
434                }
435                let slot_b = &self.slots[*idx_b];
436                if slot_a.local == slot_b.local {
437                    continue;
438                }
439                if self.may_alias(*idx_a, *idx_b) {
440                    let mut pair =
441                        crate::analysis::alias::AliasPair::new(slot_a.local, slot_b.local);
442                    pair.lhs_fields = fields_a.clone();
443                    pair.rhs_fields = fields_b.clone();
444                    pairs.add_alias(pair);
445                }
446            }
447            // Field ↔ Base (cross-level)
448            for &base_local in &local_ids {
449                if slot_a.local == base_local {
450                    continue;
451                }
452                let Some(&base_slot_idx) = local_to_base_slot.get(&base_local) else {
453                    continue;
454                };
455                if self.may_alias(*idx_a, base_slot_idx) {
456                    let mut pair = crate::analysis::alias::AliasPair::new(slot_a.local, base_local);
457                    pair.lhs_fields = fields_a.clone();
458                    pair.rhs_fields = vec![];
459                    pairs.add_alias(pair);
460                }
461            }
462        }
463
464        // Compress field paths: truncate each side to its first element,
465        // matching the old MoP alias analysis behavior.
466        pairs.compress_fields();
467
468        pairs.sort_alias_index();
469        pairs
470    }
471
472    // ── Alias partition (Union-Find for value-equivalence) ──────────
473
474    /// Find the representative of `idx`'s alias partition.
475    fn alias_find(&self, idx: usize) -> usize {
476        if idx >= self.alias_parent.len() {
477            return idx;
478        }
479        let mut cur = idx;
480        while self.alias_parent[cur] != cur {
481            cur = self.alias_parent[cur];
482        }
483        cur
484    }
485
486    /// Union two alias partitions.
487    fn alias_union(&mut self, a: usize, b: usize) {
488        let ra = self.alias_find(a);
489        let rb = self.alias_find(b);
490        if ra != rb {
491            self.alias_parent[ra] = rb;
492        }
493    }
494
495    /// Partitions whose value `idx` may hold, following `may_hold` edges.
496    fn held_partitions(&self, idx: usize) -> FxHashSet<usize> {
497        let mut roots = FxHashSet::default();
498        roots.insert(self.alias_find(idx));
499        let mut changed = true;
500        while changed {
501            changed = false;
502            for &(holder, held) in &self.may_hold {
503                if roots.contains(&self.alias_find(holder)) && roots.insert(self.alias_find(held)) {
504                    changed = true;
505                }
506            }
507        }
508        roots
509    }
510
511    /// Move `slot_idx` from its current partition to `target_idx`'s partition.
512    /// This implements the strong-update semantics of MoP's `assign_alias`:
513    /// the moved slot leaves its old partition behind.
514    fn alias_move_to_partition(&mut self, slot_idx: usize, target_idx: usize) {
515        if slot_idx >= self.alias_parent.len() {
516            return;
517        }
518        // Point slot_idx directly to target's root
519        let target_root = self.alias_find(target_idx);
520        self.alias_parent[slot_idx] = target_root;
521        self.may_hold.retain(|&(holder, _)| holder != slot_idx);
522    }
523
524    /// Strong-update: put all slots in `slot_idx`'s partition into their
525    /// own singleton partitions, breaking all alias-equivalence for the
526    /// entire partition. Used when a call produces a fresh value that
527    /// must not retain any old alias relationships.
528    pub fn reset_partition(&mut self, slot_idx: usize) {
529        if slot_idx >= self.alias_parent.len() {
530            return;
531        }
532        let root = self.alias_find(slot_idx);
533        for i in 0..self.alias_parent.len() {
534            if self.alias_find(i) == root {
535                self.alias_parent[i] = i;
536            }
537        }
538        self.may_hold.retain(|&(holder, _)| holder != slot_idx);
539    }
540
541    // ── PlaceKey-oriented adapter methods ──────────────────────────
542
543    /// Record that `pointer` place was derived from `source` place.
544    /// Strong-update semantics: clears old points-to info for the pointer.
545    pub fn insert_place_edge(&mut self, pointer: &PlaceKey, source: &PlaceKey) {
546        let ptr_slot = Self::place_key_to_slot(pointer);
547        let src_slot = Self::place_key_to_slot(source);
548        let ptr_idx = self.ensure_slot(ptr_slot, false, false);
549        self.ensure_slot(src_slot.clone(), false, false);
550        self.assign_pointee(ptr_idx, AbstractLoc::Slot(src_slot));
551    }
552
553    /// Single-step points-to lookup (non-transitive) with overlap semantics.
554    /// When the exact place has no edge, falls back through field-stripping.
555    pub fn get_place_source(&self, place: &PlaceKey) -> Option<PlaceKey> {
556        let mut slot = Self::place_key_to_slot(place);
557        loop {
558            if let Some(idx) = self.slot_index.get(&slot) {
559                if let Some(first_loc) = self.points_to.get(*idx).and_then(|set| set.iter().next())
560                {
561                    if let AbstractLoc::Slot(target) = first_loc {
562                        return Some(Self::slot_to_place_key(target));
563                    }
564                }
565            }
566            if slot.fields.is_empty() {
567                return None;
568            }
569            slot.fields.pop();
570        }
571    }
572
573    /// Transitive points-to resolution with overlap semantics and loop
574    /// detection.
575    pub fn resolve_place(&self, place: &PlaceKey) -> PlaceKey {
576        let mut cur = place.clone();
577        let mut seen: Vec<PlaceKey> = vec![cur.clone()];
578        loop {
579            let Some(next) = self.get_place_source(&cur) else {
580                break;
581            };
582            if seen.iter().any(|p| p == &next) {
583                break;
584            }
585            seen.push(next.clone());
586            cur = next.clone();
587        }
588        cur
589    }
590
591    /// Return all PlaceKey-based points-to edges.
592    pub fn place_edges(&self) -> Vec<(PlaceKey, PlaceKey)> {
593        let mut edges = Vec::new();
594        for (idx, targets) in self.points_to.iter().enumerate() {
595            let Some(slot) = self.slots.get(idx) else {
596                continue;
597            };
598            let pointer = Self::slot_to_place_key(slot);
599            for target in targets {
600                if let AbstractLoc::Slot(target_slot) = target {
601                    let source = Self::slot_to_place_key(target_slot);
602                    edges.push((pointer.clone(), source));
603                }
604            }
605        }
606        edges
607    }
608
609    fn place_key_to_slot(pk: &PlaceKey) -> Slot {
610        let local = pk.local().map(|l| l.as_usize()).unwrap_or(0);
611        Slot {
612            local,
613            fields: pk.fields.clone(),
614        }
615    }
616
617    fn slot_to_place_key(slot: &Slot) -> PlaceKey {
618        PlaceKey {
619            base: PlaceBaseKey::Local(slot.local),
620            fields: slot.fields.clone(),
621        }
622    }
623}
624
625impl Default for PtsGraph {
626    fn default() -> Self {
627        Self::new()
628    }
629}
630
631// ── Internal helpers for transitive search ─────────────────────────
632
633#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
634enum Start {
635    Pointee(usize),
636}
637
638#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
639enum Visit {
640    Pointee(usize),
641}