1
1
# Constraint propagation
2
2
3
- The main work of the region inference is ** constraint
4
- propagation** . This means processing the set of constraints to compute
5
- the final values for all the region variables.
3
+ The main work of the region inference is ** constraint propagation** ,
4
+ which is done in the [ ` propagate_constraints ` ] function. There are
5
+ three sorts of constraints that are used in NLL, and we'll explain how
6
+ ` propagate_constraints ` works by "layering" those sorts of constraints
7
+ on one at a time (each of them is fairly independent from the others):
6
8
7
- ## Kinds of constraints
9
+ - liveness constraints (` R live at E ` ), which arise from liveness;
10
+ - outlives constraints (` R1: R2 ` ), which arise from subtyping;
11
+ - [ member constraints] [ m_c ] (` member R_m of [R_c...] ` ), which arise from impl Trait.
8
12
9
- Each kind of constraint is handled somewhat differently by the region inferencer.
13
+ [ `propagate_constraints` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/struct.RegionInferenceContext.html#method.propagate_constraints
14
+ [ m_c ] : ./member_constraints.html
15
+
16
+ In this chapter, we'll explain the "heart" of constraint propagation,
17
+ covering both liveness and outlives constraints.
18
+
19
+ ## Notation and high-level concepts
20
+
21
+ Conceptually, region inference is a "fixed-point" computation. It is
22
+ given some set of constraints ` {C} ` and it computes a set of values
23
+ ` Values: R -> {E} ` that maps each region ` R ` to a set of elements
24
+ ` {E} ` (see [ here] [ riv ] for more notes on region elements):
25
+
26
+ - Initially, each region is mapped to an empty set, so `Values(R) =
27
+ {}` for all regions ` R`.
28
+ - Next, we process the constraints repeatedly until a fixed-point is reached:
29
+ - For each constraint C:
30
+ - Update ` Values ` as needed to satisfy the constraint
10
31
11
- ### Liveness constraints
32
+ [ riv ] : ../region-inference.html#region-variables
33
+
34
+ As a simple example, if we have a liveness constraint ` R live at E ` ,
35
+ then we can apply ` Values(R) = Values(R) union {E} ` to make the
36
+ constraint be satisfied. Similarly, if we have an outlives constraints
37
+ ` R1: R2 ` , we can apply ` Values(R1) = Values(R1) union Values(R2) ` .
38
+ (Member constraints are more complex and we discuss them below.)
39
+
40
+ In practice, however, we are a bit more clever. Instead of applying
41
+ the constraints in a loop, we can analyze the constraints and figure
42
+ out the correct order to apply them, so that we only have to apply
43
+ each constraint once in order to find the final result.
44
+
45
+ Similarly, in the implementation, the ` Values ` set is stored in the
46
+ ` scc_values ` field, but they are indexed not by a * region* but by a
47
+ * strongly connected component* (SCC). SCCs are an optimization that
48
+ avoids a lot of redundant storage and computation. They are explained
49
+ in the section on outlives constraints.
50
+
51
+ ## Liveness constraints
12
52
13
53
A ** liveness constraint** arises when some variable whose type
14
54
includes a region R is live at some point P. This simply means that
15
55
the value of R must include the point P. Liveness constraints are
16
56
computed by the MIR type checker.
17
57
18
- We represent them by keeping a (sparse) bitset for each region
19
- variable, which is the field [ ` liveness_constraints ` ] , of type
20
- [ ` LivenessValues ` ]
58
+ A liveness constraint ` R live at E ` is satisfied if ` E ` is a member of
59
+ ` Values(R) ` . So to "apply" such a constraint to ` Values ` , we just have
60
+ to compute ` Values(R) = Values(R) union {E} ` .
61
+
62
+ The liveness values are computed in the type-check and passes to the
63
+ region inference upon creation in the ` liveness_constraints ` argument.
64
+ These are not represented as individual constraints like ` R live at E `
65
+ though; instead, we store a (sparse) bitset per region variable (of
66
+ type [ ` LivenessValues ` ] ). This way we only need a single bit for each
67
+ liveness constraint.
21
68
22
69
[ `liveness_constraints` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/struct.RegionInferenceContext.html#structfield.liveness_constraints
23
70
[ `LivenessValues` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/values/struct.LivenessValues.html
24
71
25
- ### Outlives constraints
72
+ One thing that is worth mentioning: All lifetime parameters are always
73
+ considered to be live over the entire function body. This is because
74
+ they correspond to some portion of the * caller's* execution, and that
75
+ execution clearly includes the time spent in this function, since the
76
+ caller is waiting for us to return.
26
77
27
- An outlives constraint ` 'a: 'b ` indicates that the value of ` 'a ` must
28
- be a ** superset** of the value of ` 'b ` . On creation, we are given a
29
- set of outlives constraints in the form of a
30
- [ ` ConstraintSet ` ] . However, to work more efficiently with outlives
31
- constraints, they are [ converted into the form of a graph] [ graph-fn ] ,
32
- where the nodes of the graph are region variables (` 'a ` , ` 'b ` ) and
33
- each constraint ` 'a: 'b ` induces an edge ` 'a -> 'b ` . This conversion
34
- happens in the [ ` RegionInferenceContext::new ` ] function that creates
35
- the inference context.
36
-
37
- [ `ConstraintSet` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/constraints/struct.ConstraintSet.html
38
- [ graph-fn ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/constraints/struct.ConstraintSet.html#method.graph
39
- [ `RegionInferenceContext::new` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/struct.RegionInferenceContext.html#method.new
78
+ ## Outlives constraints
40
79
41
- ### Member constraints
80
+ An outlives constraint ` 'a: 'b ` indicates that the value of ` 'a ` must
81
+ be a ** superset** of the value of ` 'b ` . That is, an outlives
82
+ constraint ` R1: R2 ` is satisfied if ` Values(R1) ` is a superset of
83
+ ` Values(R2) ` . So to "apply" such a constraint to ` Values ` , we just
84
+ have to compute ` Values(R1) = Values(R1) union Values(R2) ` .
42
85
43
- A member constraint ` 'm member of ['c_1..'c_N] ` expresses that the
44
- region ` 'm ` must be * equal* to some ** choice regions** ` 'c_i ` (for
45
- some ` i ` ). These constraints cannot be expressed by users, but they arise
46
- from ` impl Trait ` due to its lifetime capture rules. Consinder a function
47
- such as the following:
86
+ One observation that follows from this is that if you have ` R1: R2 `
87
+ and ` R2: R1 ` , then ` R1 = R2 ` must be true. Similarly, if you have:
48
88
49
- ``` rust
50
- fn make (a : & 'a u32 , b : & 'b u32 ) -> impl Trait <'a , 'b > { .. }
51
89
```
52
-
53
- Here, the true return type (often called the "hidden type") is only
54
- permitted to capture the lifeimes ` 'a ` or ` 'b ` . You can kind of see
55
- this more clearly by desugaring that ` impl Trait ` return type into its
56
- more explicit form:
57
-
58
- ``` rust
59
- type MakeReturn <'x , 'y > = impl Trait <'x , 'y >;
60
- fn make (a : & 'a u32 , b : & 'b u32 ) -> MakeReturn <'a , 'b > { .. }
90
+ R1: R2
91
+ R2: R3
92
+ R3: R4
93
+ R4: R1
61
94
```
62
95
63
- Here, the idea is that the hidden type must be some type that could
64
- have been written in place of the ` impl Trait<'x, 'y> ` -- but clearly
65
- such a type can only reference the regions ` 'x ` or ` 'y ` (or
66
- ` 'static ` !), as those are the only names in scope. This limitation is
67
- then translated into a restriction to only access ` 'a ` or ` 'b ` because
68
- we are returning ` MakeReturn<'a, 'b> ` , where ` 'x ` and ` 'y ` have been
69
- replaced with ` 'a ` and ` 'b ` respectively.
96
+ then ` R1 = R2 = R3 = R4 ` follows. We take advantage of this to make things
97
+ much faster, as described shortly.
70
98
71
- ## SCCs in the outlives constraint graph
99
+ In the code, the set of outlives constraints is given to the region
100
+ inference context on creation in a parameter of type
101
+ [ ` ConstraintSet ` ] . The constraint set is basically just a list of `'a:
102
+ 'b` constraints.
72
103
73
- The most common sort of constraint in practice are outlives
74
- constraints like ` 'a: 'b ` . Such a cosntraint means that ` 'a ` is a
75
- superset of ` 'b ` . So what happens if we have two regions ` 'a ` and ` 'b `
76
- that mutually outlive one another, like so?
104
+ ### The outlives constraint graph and SCCs
77
105
78
- ```
79
- 'a: 'b
80
- 'b: 'a
81
- ```
106
+ In order to work more efficiently with outlives constraints, they are
107
+ [ converted into the form of a graph] [ graph-fn ] , where the nodes of the
108
+ graph are region variables (` 'a ` , ` 'b ` ) and each constraint ` 'a: 'b `
109
+ induces an edge ` 'a -> 'b ` . This conversion happens in the
110
+ [ ` RegionInferenceContext::new ` ] function that creates the inference
111
+ context.
112
+
113
+ [ `ConstraintSet` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/constraints/struct.ConstraintSet.html
114
+ [ graph-fn ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/constraints/struct.ConstraintSet.html#method.graph
115
+ [ `RegionInferenceContext::new` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/struct.RegionInferenceContext.html#method.new
82
116
83
- In this case, we can conclude that ` 'a ` and ` 'b ` must be equal
84
- sets. In fact, it doesn't have to be just two regions. We could create
85
- an extended "chain" of outlives constraints:
117
+ When using a graph representation, we can detect regions that must be equal
118
+ by looking for cycles. That is, if you have a constraint like
86
119
87
120
```
88
121
'a: 'b
@@ -91,11 +124,8 @@ an extended "chain" of outlives constraints:
91
124
'd: 'a
92
125
```
93
126
94
- Here, we know that ` 'a..'d ` are all equal to one another.
95
-
96
- As mentioned above, an outlives constraint like ` 'a: 'b ` can be viewed
97
- as an edge in a graph ` 'a -> 'b ` . Cycles in this graph indicate regions
98
- that mutually outlive one another and hence must be equal.
127
+ then this will correspond to a cycle in the graph containing the
128
+ elements ` 'a...'d ` .
99
129
100
130
Therefore, one of the first things that we do in propagating region
101
131
values is to compute the ** strongly connected components** (SCCs) in
@@ -138,9 +168,55 @@ superset of the value of `S1`. One crucial thing is that this graph of
138
168
SCCs is always a DAG -- that is, it never has cycles. This is because
139
169
all the cycles have been removed to form the SCCs themselves.
140
170
141
- ## How constraint propagation works
171
+ ### Applying liveness constraints to SCCs
172
+
173
+ The liveness constraints that come in from the type-checker are
174
+ expressed in terms of regions -- that is, we have a map like
175
+ ` Liveness: R -> {E} ` . But we want our final result to be expressed
176
+ in terms of SCCs -- we can integrate these liveness constraints very
177
+ easily just by taking the union:
178
+
179
+ ```
180
+ for each region R:
181
+ let S by the SCC that contains R
182
+ Values(S) = Values(S) union Liveness(R)
183
+ ```
184
+
185
+ In the region inferencer, this step is done in [ ` RegionInferenceContext::new ` ] .
186
+
187
+ ### Applying outlives constraints
188
+
189
+ Once we have computed the DAG of SCCs, we use that to structure out
190
+ entire computation. If we have an edge ` S1 -> S2 ` between two SCCs,
191
+ that means that ` Values(S1) >= Values(S2) ` must hold. So, to compute
192
+ the value of ` S1 ` , we first compute the values of each successor ` S2 ` .
193
+ Then we simply union all of those values together. To use a
194
+ quasi-iterator-like notation:
195
+
196
+ ```
197
+ Values(S1) =
198
+ s1.successors()
199
+ .map(|s2| Values(s2))
200
+ .union()
201
+ ```
202
+
203
+ In the code, this work starts in the [ ` propagate_constraints ` ]
204
+ function, which iterates over all the SCCs. For each SCC S1, we
205
+ compute its value by first computing the value of its
206
+ successors. Since SCCs form a DAG, we don't have to be conecrned about
207
+ cycles, though we do need to keep a set around to track whether we
208
+ have already processed a given SCC or not. For each successor S2, once
209
+ we have computed S2's value, we can union those elements into the
210
+ value for S1. (Although we have to be careful in this process to
211
+ properly handle [ higher-ranked
212
+ placeholders] ( ./placeholders_and_universes.html ) . Note that the value
213
+ for S1 already contains the liveness constraints, since they were
214
+ added in [ ` RegionInferenceContext::new ` ] .
215
+
216
+ Once that process is done, we now have the "minimal value" for S1,
217
+ taking into account all of the liveness and outlives
218
+ constraints. However, in order to complete the process, we must also
219
+ consider [ member constraints] [ m_c ] , which are described in [ a later
220
+ section] [ m_c ] .
142
221
143
- The main work of constraint propagation is done in the
144
- ` propagation_constraints ` function.
145
222
146
- [ `propagate_constraints` ] : https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir/borrow_check/nll/region_infer/struct.RegionInferenceContext.html#method.propagate_constraints
0 commit comments