Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
4f9c30f
Add initial version of value analysis and dataflow constant propagation
jachris Aug 25, 2022
d0afe68
Try field type normalization instead of forcing it
jachris Aug 29, 2022
601fcc4
Update test results
jachris Aug 29, 2022
56ff16d
Fix spelling
jachris Aug 29, 2022
93ee806
Update test results
jachris Aug 29, 2022
c83489c
Remove empty test
jachris Aug 30, 2022
bb16397
Clarify registration and tracking of references
jachris Aug 30, 2022
2928694
Add additional flooding when assigning a value and corresponding test
jachris Aug 31, 2022
e75ad93
Begin a semi-formal argument for correctness
jachris Aug 31, 2022
1da3033
Change test from usize to i32 to prevent target issues
jachris Aug 31, 2022
3f98dc7
Clarify place expressions vs place objects
jachris Sep 1, 2022
ad99d2e
Move handling of references and simplify flooding
jachris Sep 1, 2022
47a00d5
Flood with bottom instead of top for unreachable branches
jachris Sep 1, 2022
8a789ce
Reject registration of downcasts for now
jachris Sep 1, 2022
469fb19
Update other test results
jachris Sep 1, 2022
16dedba
Ignore terminators explicitly
jachris Sep 1, 2022
fe84bbf
Add tracking of unreachability
jachris Sep 2, 2022
f234419
Rebase onto master
jachris Sep 2, 2022
895181b
Remove leftover test files
jachris Sep 2, 2022
2e4d082
Add more documentation
jachris Sep 2, 2022
2113e45
Remove superfluous line
jachris Sep 2, 2022
904adca
Flood place on drop
jachris Sep 4, 2022
1e5ca57
Use StorageDead and Deinit to flood place
jachris Sep 4, 2022
e2ddf8a
Add comment about downcast projection element
jachris Sep 4, 2022
817c277
Handle StorageLive
jachris Sep 9, 2022
bc82c13
Track Scalar instead of ScalarInt for const prop
jachris Sep 23, 2022
13b7059
Only allow registration of scalars for now
jachris Sep 27, 2022
6868617
Add tests from current const prop
jachris Sep 27, 2022
4cda6e5
Update test results
jachris Sep 27, 2022
97a69a7
Add some more unit-test directives
jachris Sep 27, 2022
c56e99c
Fix typo
jachris Sep 27, 2022
f99950f
Update test results after rebase
jachris Oct 4, 2022
eab7732
Handle NonDivergingIntrinsic and CopyNonOverlapping
jachris Oct 5, 2022
2f66e94
Flood with bottom for Deinit, StorageLive and StorageDead
jachris Oct 5, 2022
b5063ab
Make more assumptions explicit
jachris Oct 5, 2022
1765587
Only track (trivially) freeze types
jachris Oct 5, 2022
7ab1ba9
Remove `Unknown` state in favor of `Value(Top)`
jachris Oct 5, 2022
4478a87
Fix formatting
jachris Oct 5, 2022
111324e
Prevent registration inside references if target is !Freeze
jachris Oct 6, 2022
3c0f3b0
Only assume Stacked Borrows if -Zunsound-mir-opts is given
jachris Oct 6, 2022
7a52e73
Add tests for Stacked Borrows behavior
jachris Oct 6, 2022
b9dbb81
Improve example used for SB tests
jachris Oct 6, 2022
5696d06
Use the same is_enabled as the current const prop
jachris Oct 6, 2022
aaa35b3
Add comment for the current retag situation
jachris Oct 12, 2022
890fae9
Fix rebased CastKind
jachris Oct 12, 2022
8bed0b5
Update issue-50814.rs test result
jachris Oct 15, 2022
1dde908
Update test results
jachris Oct 15, 2022
be9013f
Make overflow flag propagation conditional
jachris Oct 15, 2022
931d99f
Make overflow handling more precise
jachris Oct 15, 2022
274a491
Improve documentation, plus some small changes
jachris Oct 19, 2022
062053b
Fix unimplemented binary_ptr_op
jachris Oct 19, 2022
5b7b309
Improve documentation of assumptions
jachris Oct 21, 2022
d86acdd
Prevent propagation of overflow if overflow occured
jachris Oct 23, 2022
de69d08
Explicitly match all terminators
jachris Oct 23, 2022
efc7ca8
Use ParamEnv consistently
jachris Oct 23, 2022
f29533b
Small documentation changes
jachris Oct 24, 2022
1f82a9f
Move HasTop and HasBottom into lattice.rs
jachris Oct 24, 2022
da4a40f
Remove copy of current const prop tests and add a few new tests
jachris Oct 25, 2022
630e17d
Limit number of tracked places, and some other perf improvements
jachris Oct 25, 2022
b478fcf
Use new cast methods
jachris Oct 26, 2022
72196ee
Limit number of basic blocks and tracked places to 100 for now
jachris Oct 27, 2022
89f9349
Small corrections of documentation
jachris Nov 7, 2022
3997893
Fix rebase
jachris Nov 7, 2022
bfbca6c
Completely remove tracking of references for now
jachris Nov 9, 2022
9766ee0
Fix struct field tracking and add tests for it
jachris Nov 9, 2022
8ecb276
Simplify creation of map
jachris Nov 10, 2022
3c6d1a7
Add test for repr(transparent) with scalar
jachris Nov 11, 2022
b3f6489
Add comment for guessed constants
jachris Nov 12, 2022
2e034dc
Exclude locals completely, instead of individual places
jachris Nov 12, 2022
74d53ab
Require -Zmir-opt-level >= 3 for now
jachris Nov 12, 2022
abe31a9
Partially revert 74d53ab
jachris Nov 12, 2022
d66a00a
Expand upon comment regarding self-assignment
jachris Nov 12, 2022
ea23585
Disable limits if mir-opt-level >= 4
jachris Nov 12, 2022
108790b
Remove log statement that was commented out
jachris Nov 12, 2022
c27ddc9
Remove redundant graphviz escaping
jachris Nov 14, 2022
24d2e90
Bless graphviz tests
jachris Nov 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve documentation of assumptions
  • Loading branch information
jachris committed Nov 7, 2022
commit 5b7b309c60465ec948b42332673ac3860f2d2482
116 changes: 51 additions & 65 deletions compiler/rustc_mir_dataflow/src/value_analysis.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,49 @@
//! This module provides a framework on top of the normal MIR dataflow framework to simplify the
//! implementation of analyses that track the values stored in places of interest.
//! implementation of analyses that track information about the values stored in certain places.
//! We are using the term "place" here to refer to a `mir::Place` (a place expression) instead of
//! an `interpret::Place` (a memory location).
//!
//! The default methods of [`ValueAnalysis`] (prefixed with `super_` instead of `handle_`)
//! provide some behavior that should be valid for all abstract domains that are based only on the
//! value stored in a certain place. On top of these default rules, an implementation should
//! override some of the `handle_` methods. For an example, see `ConstAnalysis`.
//!
//! An implementation must also provide a [`Map`]. Before the anaylsis begins, all places that
//! should be tracked during the analysis must be registered. Currently, the projections of these
//! places may only contain derefs, fields and downcasts (otherwise registration fails). During the
//! analysis, no new places can be registered.
//! An implementation must also provide a [`Map`]. Before the analysis begins, all places that
//! should be tracked during the analysis must be registered. During the analysis, no new places
//! can be registered. The [`State`] can be queried to retrieve the abstract value stored for a
//! certain place by passing the map.
//!
//! Note that if you want to track values behind references, you have to register the dereferenced
//! place. For example: Assume `let x = (0, 0)` and that we want to propagate values from `x.0` and
//! `x.1` also through the assignment `let y = &x`. In this case, we should register `x.0`, `x.1`,
//! `(*y).0` and `(*y).1`.
//! This framework is currently experimental. In particular, the features related to references are
//! currently guarded behind `-Zunsound-mir-opts`, because their correctness relies on Stacked
//! Borrows. Also, only places with scalar types can be tracked currently. This is because scalar
//! types are indivisible, which simplifies the current implementation. But this limitation could be
//! lifted in the future.
//!
//!
//! # Correctness
//! # Notes
//!
//! Warning: This is a semi-formal attempt to argue for the correctness of this analysis. If you
//! find any weak spots, let me know! Recommended reading: Abstract Interpretation. We will use the
//! term "place" to refer to a place expression (like `mir::Place`), and we will call the
//! underlying entity "object". For instance, `*_1` and `*_2` are not the same place, but depending
//! on the value of `_1` and `_2`, they could refer to the same object. Also, the same place can
//! refer to different objects during execution. If `_1` is reassigned, then `*_1` may refer to
//! different objects before and after assignment. Additionally, when saying "access to a place",
//! what we really mean is "access to an object denoted by arbitrary projections of that place".
//! - The bottom state denotes uninitialized memory. Because we are only doing a sound approximation
//! of the actual execution, we can also use this state for places where access would be UB.
//!
//! In the following, we will assume a constant propagation analysis. Our analysis is correct if
//! every transfer function is correct. This is the case if for every pair (f, f#) and abstract
//! state s, we have f(y(s)) <= y(f#(s)), where s is a mapping from tracked place to top, bottom or
//! a constant. Since pointers (and mutable references) are not tracked, but can be used to change
//! values in the concrete domain, f# must assume that all places that can be affected in this way
//! for a given program point are already marked with top in s (otherwise many assignments and
//! function calls would have no choice but to mark all tracked places with top). This leads us to
//! an invariant: For all possible program points where there could possibly exist means of mutable
//! access to a tracked place (in the concrete domain), this place must be assigned to top (in the
//! abstract domain). The concretization function y can be defined as expected for the constant
//! propagation analysis, although the concrete state of course contains all kinds of non-tracked
//! data. However, by the invariant above, no mutable access to tracked places that are not marked
//! with top may be introduced.
//! - The assignment logic in `State::assign_place_idx` assumes that the places are non-overlapping,
//! or identical. Note that this refers to place expressions, not memory locations.
//!
//! Note that we (at least currently) do not differentiate between "this place may assume different
//! values" and "a pointer to this place escaped the analysis". However, we still want to handle
//! assignments to constants as usual for f#. This adds an assumption: Whenever we have an
//! assignment that is captured by the analysis, all mutable access to the underlying place (which
//! is not observable by the analysis) must be invalidated. This is (hopefully) covered by Stacked
//! Borrows.
//! - Since pointers (and mutable references) are not tracked, but can be used to change the
//! underlying values, we are conservative and immediately flood the referenced place upon creation
//! of the pointer. Also, we have to uphold the invariant that the place must stay that way as long
//! as this mutable access could exist. However...
//!
//! To be continued...
//! - Without an aliasing model like Stacked Borrows (i.e., `-Zunsound-mir-opts` is not given),
//! such mutable access is never revoked. And even shared references could be used to obtain the
//! address of a value an modify it. When not assuming Stacked Borrows, we prevent such places from
//! being tracked at all. This means that the analysis itself can assume that writes to a *tracked*
//! place always invalidate all other means of mutable access, regardless of the aliasing model.
//!
//! The bottom state denotes uninitalized memory.
//!
//!
//! # Assumptions
//!
//! - (A1) Assignment to any tracked place invalidates all pointers that could be used to change
//! the underlying value.
//! - (A2) `StorageLive`, `StorageDead` and `Deinit` make the underlying memory at least
//! uninitialized (at least in the sense that declaring access UB is also fine).
//! - (A3) An assignment with `State::assign_place_idx` either involves non-overlapping places, or
//! the places are the same.
//! - (A4) If the value behind a reference to a `Freeze` place is changed, dereferencing the
//! reference is UB.
//! - Likewise, the analysis itself assumes that if the value of a *tracked* place behind a shared
//! reference is changed, the reference may not be used to access that value anymore. This is true
//! for all places if the referenced type is `Freeze` and we assume Stacked Borrows. If we are not
//! assuming Stacking Borrows (or if the referenced type could be `!Freeze`), we again prevent such
//! places from being tracked at all, making this assertion trivially true.

use std::fmt::{Debug, Formatter};

Expand Down Expand Up @@ -107,11 +87,12 @@ pub trait ValueAnalysis<'tcx> {
self.handle_intrinsic(intrinsic, state);
}
StatementKind::StorageLive(local) | StatementKind::StorageDead(local) => {
// (A2)
// StorageLive leaves the local in an uninitialized state.
// StorageDead makes it UB to access the local afterwards.
state.flood_with(Place::from(*local).as_ref(), self.map(), Self::Value::bottom());
}
StatementKind::Deinit(box place) => {
// (A2)
// Deinit makes the place uninitialized.
state.flood_with(place.as_ref(), self.map(), Self::Value::bottom());
}
StatementKind::Retag(..) => {
Expand Down Expand Up @@ -477,9 +458,9 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
/// Copies `source` to `target`, including all tracked places beneath.
///
/// If `target` contains a place that is not contained in `source`, it will be overwritten with
/// Top. Also, because this will copy all entries one after another, it may only be
/// Top. Also, because this will copy all entries one after another, it may only be used for
/// places that are non-overlapping or identical.
pub fn assign_place_idx(&mut self, target: PlaceIndex, source: PlaceIndex, map: &Map) {
// We use (A3) and copy all entries one after another.
let StateData::Reachable(values) = &mut self.0 else { return };

// If both places are tracked, we copy the value to the target. If the target is tracked,
Expand Down Expand Up @@ -528,21 +509,24 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
if let Some(value_index) = map.places[target].value_index {
values[value_index] = V::top();
}
// Instead of tracking of *where* a reference points to (as in, which place), we
// track *what* it points to (as in, what do we know about the target). For an
// assignment `x = &y`, we thus copy the info we have for `y` to `*x`. This is
// sound because we only track places that are `Freeze`, and (A4).
// Instead of tracking of *where* a reference points to (as in, which memory
// location), we track *what* it points to (as in, what do we know about the
// target). For an assignment `x = &y`, we thus copy the info of `y` to `*x`.
if let Some(target_deref) = map.apply(target, TrackElem::Deref) {
// We know here that `*x` is `Freeze`, because we only track through
// dereferences if the target type is `Freeze`.
self.assign_place_idx(target_deref, source, map);
}
}
}
}

/// Retrieve the value stored for a place, or ⊥ if it is not tracked.
pub fn get(&self, place: PlaceRef<'_>, map: &Map) -> V {
map.find(place).map(|place| self.get_idx(place, map)).unwrap_or(V::top())
}

/// Retrieve the value stored for a place index, or ⊥ if it is not tracked.
pub fn get_idx(&self, place: PlaceIndex, map: &Map) -> V {
match &self.0 {
StateData::Reachable(values) => {
Expand All @@ -569,11 +553,11 @@ impl<V: JoinSemiLattice + Clone> JoinSemiLattice for State<V> {
}
}

/// A partial mapping from `Place` to `PlaceIndex`.
/// A partial mapping from `Place` to `PlaceIndex`, where some place indices have value indices.
///
/// Some additioanl bookkeeping is done to speed up traversal:
/// Some additional bookkeeping is done to speed up traversal:
/// - For iteration, every [`PlaceInfo`] contains an intrusive linked list of its children.
/// - To directly get the child for a specific projection, there is `projections` map.
/// - To directly get the child for a specific projection, there is a `projections` map.
#[derive(Debug)]
pub struct Map {
locals: IndexVec<Local, Option<PlaceIndex>>,
Expand All @@ -595,7 +579,7 @@ impl Map {
/// Returns a map that only tracks places whose type passes the filter.
///
/// This is currently the only way to create a [`Map`]. The way in which the tracked places are
/// chosen is an implementation detail an may not be relied upon.
/// chosen is an implementation detail and may not be relied upon.
pub fn from_filter<'tcx>(
tcx: TyCtxt<'tcx>,
body: &Body<'tcx>,
Expand All @@ -609,7 +593,7 @@ impl Map {
// not yet guaranteed).
if tcx.sess.opts.unstable_opts.unsound_mir_opts {
// We might want to add additional limitations. If a struct has 10 boxed fields of
// itself, will currently be `10.pow(max_derefs)` tracked places.
// itself, there will currently be `10.pow(max_derefs)` tracked places.
map.register_with_filter(tcx, body, 2, filter, &[]);
} else {
map.register_with_filter(tcx, body, 0, filter, &escaped_places(body));
Expand Down Expand Up @@ -668,7 +652,7 @@ impl Map {

if max_derefs > 0 {
if let Some(ty::TypeAndMut { ty: deref_ty, .. }) = ty.builtin_deref(false) {
// References can only be tracked if the target is `!Freeze`.
// Values behind references can only be tracked if the target is `Freeze`.
if deref_ty.is_freeze(tcx.at(DUMMY_SP), param_env) {
projection.push(PlaceElem::Deref);
self.register_with_filter_rec(
Expand Down Expand Up @@ -953,6 +937,8 @@ fn iter_fields<'tcx>(
}

/// Returns all places, that have their reference or address taken.
///
/// This includes shared references.
fn escaped_places<'tcx>(body: &Body<'tcx>) -> Vec<Place<'tcx>> {
struct Collector<'tcx> {
result: Vec<Place<'tcx>>,
Expand Down