11//! This module provides a framework on top of the normal MIR dataflow framework to simplify the
2- //! implementation of analyses that track the values stored in places of interest.
2+ //! implementation of analyses that track information about the values stored in certain places.
3+ //! We are using the term "place" here to refer to a `mir::Place` (a place expression) instead of
4+ //! an `interpret::Place` (a memory location).
35//!
46//! The default methods of [`ValueAnalysis`] (prefixed with `super_` instead of `handle_`)
57//! provide some behavior that should be valid for all abstract domains that are based only on the
68//! value stored in a certain place. On top of these default rules, an implementation should
79//! override some of the `handle_` methods. For an example, see `ConstAnalysis`.
810//!
9- //! An implementation must also provide a [`Map`]. Before the anaylsis begins, all places that
10- //! should be tracked during the analysis must be registered. Currently, the projections of these
11- //! places may only contain derefs, fields and downcasts (otherwise registration fails). During the
12- //! analysis, no new places can be registered .
11+ //! An implementation must also provide a [`Map`]. Before the analysis begins, all places that
12+ //! should be tracked during the analysis must be registered. During the analysis, no new places
13+ //! can be registered. The [`State`] can be queried to retrieve the abstract value stored for a
14+ //! certain place by passing the map .
1315//!
14- //! Note that if you want to track values behind references, you have to register the dereferenced
15- //! place. For example: Assume `let x = (0, 0)` and that we want to propagate values from `x.0` and
16- //! `x.1` also through the assignment `let y = &x`. In this case, we should register `x.0`, `x.1`,
17- //! `(*y).0` and `(*y).1`.
16+ //! This framework is currently experimental. In particular, the features related to references are
17+ //! currently guarded behind `-Zunsound-mir-opts`, because their correctness relies on Stacked
18+ //! Borrows. Also, only places with scalar types can be tracked currently. This is because scalar
19+ //! types are indivisible, which simplifies the current implementation. But this limitation could be
20+ //! lifted in the future.
1821//!
1922//!
20- //! # Correctness
23+ //! # Notes
2124//!
22- //! Warning: This is a semi-formal attempt to argue for the correctness of this analysis. If you
23- //! find any weak spots, let me know! Recommended reading: Abstract Interpretation. We will use the
24- //! term "place" to refer to a place expression (like `mir::Place`), and we will call the
25- //! underlying entity "object". For instance, `*_1` and `*_2` are not the same place, but depending
26- //! on the value of `_1` and `_2`, they could refer to the same object. Also, the same place can
27- //! refer to different objects during execution. If `_1` is reassigned, then `*_1` may refer to
28- //! different objects before and after assignment. Additionally, when saying "access to a place",
29- //! what we really mean is "access to an object denoted by arbitrary projections of that place".
25+ //! - The bottom state denotes uninitialized memory. Because we are only doing a sound approximation
26+ //! of the actual execution, we can also use this state for places where access would be UB.
3027//!
31- //! In the following, we will assume a constant propagation analysis. Our analysis is correct if
32- //! every transfer function is correct. This is the case if for every pair (f, f#) and abstract
33- //! state s, we have f(y(s)) <= y(f#(s)), where s is a mapping from tracked place to top, bottom or
34- //! a constant. Since pointers (and mutable references) are not tracked, but can be used to change
35- //! values in the concrete domain, f# must assume that all places that can be affected in this way
36- //! for a given program point are already marked with top in s (otherwise many assignments and
37- //! function calls would have no choice but to mark all tracked places with top). This leads us to
38- //! an invariant: For all possible program points where there could possibly exist means of mutable
39- //! access to a tracked place (in the concrete domain), this place must be assigned to top (in the
40- //! abstract domain). The concretization function y can be defined as expected for the constant
41- //! propagation analysis, although the concrete state of course contains all kinds of non-tracked
42- //! data. However, by the invariant above, no mutable access to tracked places that are not marked
43- //! with top may be introduced.
28+ //! - The assignment logic in `State::assign_place_idx` assumes that the places are non-overlapping,
29+ //! or identical. Note that this refers to place expressions, not memory locations.
4430//!
45- //! Note that we (at least currently) do not differentiate between "this place may assume different
46- //! values" and "a pointer to this place escaped the analysis". However, we still want to handle
47- //! assignments to constants as usual for f#. This adds an assumption: Whenever we have an
48- //! assignment that is captured by the analysis, all mutable access to the underlying place (which
49- //! is not observable by the analysis) must be invalidated. This is (hopefully) covered by Stacked
50- //! Borrows.
31+ //! - Since pointers (and mutable references) are not tracked, but can be used to change the
32+ //! underlying values, we are conservative and immediately flood the referenced place upon creation
33+ //! of the pointer. Also, we have to uphold the invariant that the place must stay that way as long
34+ //! as this mutable access could exist. However...
5135//!
52- //! To be continued...
36+ //! - Without an aliasing model like Stacked Borrows (i.e., `-Zunsound-mir-opts` is not given),
37+ //! such mutable access is never revoked. And even shared references could be used to obtain the
38+ //! address of a value an modify it. When not assuming Stacked Borrows, we prevent such places from
39+ //! being tracked at all. This means that the analysis itself can assume that writes to a *tracked*
40+ //! place always invalidate all other means of mutable access, regardless of the aliasing model.
5341//!
54- //! The bottom state denotes uninitalized memory.
55- //!
56- //!
57- //! # Assumptions
58- //!
59- //! - (A1) Assignment to any tracked place invalidates all pointers that could be used to change
60- //! the underlying value.
61- //! - (A2) `StorageLive`, `StorageDead` and `Deinit` make the underlying memory at least
62- //! uninitialized (at least in the sense that declaring access UB is also fine).
63- //! - (A3) An assignment with `State::assign_place_idx` either involves non-overlapping places, or
64- //! the places are the same.
65- //! - (A4) If the value behind a reference to a `Freeze` place is changed, dereferencing the
66- //! reference is UB.
42+ //! - Likewise, the analysis itself assumes that if the value of a *tracked* place behind a shared
43+ //! reference is changed, the reference may not be used to access that value anymore. This is true
44+ //! for all places if the referenced type is `Freeze` and we assume Stacked Borrows. If we are not
45+ //! assuming Stacking Borrows (or if the referenced type could be `!Freeze`), we again prevent such
46+ //! places from being tracked at all, making this assertion trivially true.
6747
6848use std:: fmt:: { Debug , Formatter } ;
6949
@@ -107,11 +87,12 @@ pub trait ValueAnalysis<'tcx> {
10787 self . handle_intrinsic ( intrinsic, state) ;
10888 }
10989 StatementKind :: StorageLive ( local) | StatementKind :: StorageDead ( local) => {
110- // (A2)
90+ // StorageLive leaves the local in an uninitialized state.
91+ // StorageDead makes it UB to access the local afterwards.
11192 state. flood_with ( Place :: from ( * local) . as_ref ( ) , self . map ( ) , Self :: Value :: bottom ( ) ) ;
11293 }
11394 StatementKind :: Deinit ( box place) => {
114- // (A2)
95+ // Deinit makes the place uninitialized.
11596 state. flood_with ( place. as_ref ( ) , self . map ( ) , Self :: Value :: bottom ( ) ) ;
11697 }
11798 StatementKind :: Retag ( ..) => {
@@ -477,9 +458,9 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
477458 /// Copies `source` to `target`, including all tracked places beneath.
478459 ///
479460 /// If `target` contains a place that is not contained in `source`, it will be overwritten with
480- /// Top. Also, because this will copy all entries one after another, it may only be
461+ /// Top. Also, because this will copy all entries one after another, it may only be used for
462+ /// places that are non-overlapping or identical.
481463 pub fn assign_place_idx ( & mut self , target : PlaceIndex , source : PlaceIndex , map : & Map ) {
482- // We use (A3) and copy all entries one after another.
483464 let StateData :: Reachable ( values) = & mut self . 0 else { return } ;
484465
485466 // If both places are tracked, we copy the value to the target. If the target is tracked,
@@ -528,21 +509,24 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
528509 if let Some ( value_index) = map. places [ target] . value_index {
529510 values[ value_index] = V :: top ( ) ;
530511 }
531- // Instead of tracking of *where* a reference points to (as in, which place), we
532- // track *what* it points to (as in, what do we know about the target). For an
533- // assignment `x = &y`, we thus copy the info we have for `y` to `*x`. This is
534- // sound because we only track places that are `Freeze`, and (A4).
512+ // Instead of tracking of *where* a reference points to (as in, which memory
513+ // location), we track *what* it points to (as in, what do we know about the
514+ // target). For an assignment `x = &y`, we thus copy the info of `y` to `*x`.
535515 if let Some ( target_deref) = map. apply ( target, TrackElem :: Deref ) {
516+ // We know here that `*x` is `Freeze`, because we only track through
517+ // dereferences if the target type is `Freeze`.
536518 self . assign_place_idx ( target_deref, source, map) ;
537519 }
538520 }
539521 }
540522 }
541523
524+ /// Retrieve the value stored for a place, or ⊥ if it is not tracked.
542525 pub fn get ( & self , place : PlaceRef < ' _ > , map : & Map ) -> V {
543526 map. find ( place) . map ( |place| self . get_idx ( place, map) ) . unwrap_or ( V :: top ( ) )
544527 }
545528
529+ /// Retrieve the value stored for a place index, or ⊥ if it is not tracked.
546530 pub fn get_idx ( & self , place : PlaceIndex , map : & Map ) -> V {
547531 match & self . 0 {
548532 StateData :: Reachable ( values) => {
@@ -569,11 +553,11 @@ impl<V: JoinSemiLattice + Clone> JoinSemiLattice for State<V> {
569553 }
570554}
571555
572- /// A partial mapping from `Place` to `PlaceIndex`.
556+ /// A partial mapping from `Place` to `PlaceIndex`, where some place indices have value indices .
573557///
574- /// Some additioanl bookkeeping is done to speed up traversal:
558+ /// Some additional bookkeeping is done to speed up traversal:
575559/// - For iteration, every [`PlaceInfo`] contains an intrusive linked list of its children.
576- /// - To directly get the child for a specific projection, there is `projections` map.
560+ /// - To directly get the child for a specific projection, there is a `projections` map.
577561#[ derive( Debug ) ]
578562pub struct Map {
579563 locals : IndexVec < Local , Option < PlaceIndex > > ,
@@ -595,7 +579,7 @@ impl Map {
595579 /// Returns a map that only tracks places whose type passes the filter.
596580 ///
597581 /// This is currently the only way to create a [`Map`]. The way in which the tracked places are
598- /// chosen is an implementation detail an may not be relied upon.
582+ /// chosen is an implementation detail and may not be relied upon.
599583 pub fn from_filter < ' tcx > (
600584 tcx : TyCtxt < ' tcx > ,
601585 body : & Body < ' tcx > ,
@@ -609,7 +593,7 @@ impl Map {
609593 // not yet guaranteed).
610594 if tcx. sess . opts . unstable_opts . unsound_mir_opts {
611595 // We might want to add additional limitations. If a struct has 10 boxed fields of
612- // itself, will currently be `10.pow(max_derefs)` tracked places.
596+ // itself, there will currently be `10.pow(max_derefs)` tracked places.
613597 map. register_with_filter ( tcx, body, 2 , filter, & [ ] ) ;
614598 } else {
615599 map. register_with_filter ( tcx, body, 0 , filter, & escaped_places ( body) ) ;
@@ -668,7 +652,7 @@ impl Map {
668652
669653 if max_derefs > 0 {
670654 if let Some ( ty:: TypeAndMut { ty : deref_ty, .. } ) = ty. builtin_deref ( false ) {
671- // References can only be tracked if the target is `! Freeze`.
655+ // Values behind references can only be tracked if the target is `Freeze`.
672656 if deref_ty. is_freeze ( tcx. at ( DUMMY_SP ) , param_env) {
673657 projection. push ( PlaceElem :: Deref ) ;
674658 self . register_with_filter_rec (
@@ -953,6 +937,8 @@ fn iter_fields<'tcx>(
953937}
954938
955939/// Returns all places, that have their reference or address taken.
940+ ///
941+ /// This includes shared references.
956942fn escaped_places < ' tcx > ( body : & Body < ' tcx > ) -> Vec < Place < ' tcx > > {
957943 struct Collector < ' tcx > {
958944 result : Vec < Place < ' tcx > > ,
0 commit comments