Skip to content

Commit 45d0afb

Browse files
aliemjayoli-obk
authored andcommitted
add opaque-types-region-inference-restrictions
1 parent ffa246b commit 45d0afb

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Opaque types region inference restrictions
2+
3+
In this chapter we discuss the various restrictions we impose on the generic arguments of
4+
opaque types when defining their hidden types
5+
`Opaque<'a, 'b, .., A, B, ..> := SomeHiddenType`.
6+
7+
These restrictions are implemented in borrow checking ([Source][source-borrowck-opaque])
8+
as it is the final step opaque types inference.
9+
10+
[source-borrowck-opaque]: https://github.com/rust-lang/rust/blob/435b5255148617128f0a9b17bacd3cc10e032b23/compiler/rustc_borrowck/src/region_infer/opaque_types.rs
11+
12+
## Background: type and const generic arguments
13+
For type arguments, two restrictions are necessary: each type argument must be
14+
(1) a type parameter and
15+
(2) is unique among the generic arguments.
16+
The same is applied to const arguments.
17+
18+
Example of case (1):
19+
```rust
20+
type Opaque<X> = impl Sized;
21+
22+
// `T` is a type paramter.
23+
// Opaque<T> := ();
24+
fn good<T>() -> Opaque<T> {}
25+
26+
// `()` is not a type parameter.
27+
// Opaque<()> := ();
28+
fn bad() -> Opaque<()> {} //~ ERROR
29+
```
30+
31+
Example of case (2):
32+
```rust
33+
type Opaque<X, Y> = impl Sized;
34+
35+
// `T` and `U` are unique in the generic args.
36+
// Opaque<T, U> := T;
37+
fn good<T, U>(t: T, _u: U) -> Opaque<T, U> { t }
38+
39+
// `T` appears twice in the generic args.
40+
// Opaque<T, T> := T;
41+
fn bad<T>(t: T) -> Opaque<T, T> { t } //~ ERROR
42+
```
43+
**Motivation:** In the first case `Opaque<()> := ()`, the hidden type is ambiguous because
44+
it is compatible with two different interpretaions: `Opaque<X> := X` and `Opaque<X> := ()`.
45+
Similarily for the second case `Opaque<T, T> := T`, it is ambiguous whether it should be
46+
interpreted as `Opaque<X, Y> := X` or as `Opaque<X, Y> := Y`.
47+
Because of this ambiguity, both cases are rejected as invalid defining uses.
48+
49+
## Uniqueness restriction
50+
51+
Each lifetime argument must be unique in the arguments list and must not be `'static`.
52+
This is in order to avoid an ambiguity with hidden type inference similar to the case of
53+
type parameters.
54+
For example, the invalid defining use below `Opaque<'static> := Inv<'static>` is compatible with
55+
both `Opaque<'x> := Inv<'static>` and `Opaque<'x> := Inv<'x>`.
56+
57+
```rust
58+
type Opaque<'x> = impl Sized + 'x;
59+
type Inv<'a> = Option<*mut &'a ()>;
60+
61+
fn good<'a>() -> Opaque<'a> { Inv::<'static>::None }
62+
63+
fn bad() -> Opaque<'static> { Inv::<'static>::None }
64+
//~^ ERROR
65+
```
66+
67+
```rust
68+
type Opaque<'x, 'y> = impl Trait<'x, 'y>;
69+
70+
fn good<'a, 'b>() -> Opaque<'a, 'b> {}
71+
72+
fn bad<'a>() -> Opaque<'a, 'a> {}
73+
//~^ ERROR
74+
```
75+
76+
**Semantic lifetime equlity:**
77+
One complexity with lifetimes compared to type parameters is that
78+
two lifetimes that are syntactically different may be semantically equal.
79+
Therefore, we need to be cautious when verifying that the lifetimes are unique.
80+
81+
```rust
82+
// This is also invalid because `'a` is *semantically* equal to `'static`.
83+
fn still_bad_1<'a: 'static>() -> Opaque<'a> {}
84+
//~^ Should error!
85+
86+
// This is also invalid because `'a` and `'b` are *semantically* equal.
87+
fn still_bad_2<'a: 'b, 'b: 'a>() -> Opaque<'a, 'b> {}
88+
//~^ Should error!
89+
```
90+
91+
## An exception to uniqueness rule
92+
93+
An exception to the uniqueness rule above is when the bounds at the opaque type's definition require
94+
a lifetime parameter to be equal to another one or to the `'static` lifetime.
95+
```rust
96+
// The definition requires `'x` to be equal to `'static`.
97+
type Opaque<'x: 'static> = impl Sized + 'x;
98+
99+
fn good() -> Opaque<'static> {}
100+
```
101+
102+
**Motivation:** an attempt to implement the uniqueness restriction for RPITs resulted in a
103+
[breakage found by crater]( https://github.com/rust-lang/rust/pull/112842#issuecomment-1610057887).
104+
This can be mitigated by this exception to the rule.
105+
An example of the the code that would otherwise break:
106+
```rust
107+
struct Type<'a>(&'a ());
108+
impl<'a> Type<'a> {
109+
// `'b == 'a`
110+
fn do_stuff<'b: 'a>(&'b self) -> impl Trait<'a, 'b> {}
111+
}
112+
```
113+
114+
**Why this is correct:** for such a defining use like `Opaque<'a, 'a> := &'a str`,
115+
it can be interpreted in either way—either as `Opaque<'x, 'y> := &'x str` or as
116+
`Opaque<'x, 'y> := &'y str` and it wouldn't matter because every use of `Opaque`
117+
will guarantee that both parameters are equal as per the well-formedness rules.
118+
119+
## Universal lifetimes restriction
120+
121+
Only universally quantified lifetimes are allowed in the opaque type arguments.
122+
This includes lifetime parameters and placeholders.
123+
124+
```rust
125+
type Opaque<'x> = impl Sized + 'x;
126+
127+
fn test<'a>() -> Opaque<'a> {
128+
// `Opaque<'empty> := ()`
129+
let _: Opaque<'_> = ();
130+
//~^ ERROR
131+
}
132+
```
133+
134+
**Motivation:**
135+
This makes the lifetime and type arguments behave consistently but this is only as a bonus.
136+
The real reason behind this restriction is purely technical, as the [member constraints] algorithm
137+
faces a fundamental limitation:
138+
When encountering an opaque type definition `Opaque<'?1> := &'?2 u8`,
139+
a member constraint `'?2 member-of ['static, '?1]` is registered.
140+
In order for the algorithm to pick the right choice, the *complete* set of "outlives" relationships
141+
between the choice regions `['static, '?1]` must already be known *before* doing the region
142+
inference. This can be satisfied only if each choice region is either:
143+
1. a universal region, i.e. `RegionKind::Re{EarlyParam,LateParam,Placeholder,Static}`,
144+
because the relations between universal regions are completely known, prior to region inference,
145+
from the explicit and implied bounds.
146+
1. or an existential region that is "strictly equal" to a universal region.
147+
Strict lifetime equality is defined below and is required here because it is the only type of
148+
equality that can be evaluated prior to full region inference.
149+
150+
**Strict lifetime equality:**
151+
We say that two lifetimes are strictly equal if there are bidirectional outlives constraints
152+
between them. In NLL terms, this means the lifetimes are part of the same [SCC].
153+
Importantly this type of equality can be evaluated prior to full region inference
154+
(but of course after constraint collection).
155+
The other type of equality is when region inference ends up giving two lifetimes variables
156+
the same value even if they are not strictly equal.
157+
See [#113971] for how we used to conflate the difference.
158+
159+
[#113971]: https://github.com/rust-lang/rust/issues/113971
160+
[SCC]: https://en.wikipedia.org/wiki/Strongly_connected_component
161+
[member constraints]: https://rustc-dev-guide.rust-lang.org/borrow_check/region_inference/member_constraints.html
162+
163+
**interaction with "once modulo regions" restriction**
164+
In the example above, note the opaque type in the signature is `Opaque<'a>` and the one in the
165+
invalid defining use is `Opaque<'empty>`.
166+
In the proposed MiniTAIT plan, namely the ["once modulo regions"][#116935] rule,
167+
we already disallow this.
168+
Although it might appear that "universal lifetimes" restriction becomes redundant as it logically
169+
follows from "MiniTAIT" restrictions, the subsequent related discussion on lifetime equality and
170+
closures remains relevant.
171+
172+
[#116935]: https://github.com/rust-lang/rust/pull/116935
173+
174+
175+
## Closure restrictions
176+
177+
When the opaque type is defined in a closure/coroutine/inline-const body, universal lifetimes that
178+
are "external" to the closure are not allowed in the opaque type arguments.
179+
External regions are defined in [`RegionClassification::External`][source-external-region]
180+
181+
[source-external-region]: https://github.com/rust-lang/rust/blob/caf730043232affb6b10d1393895998cb4968520/compiler/rustc_borrowck/src/universal_regions.rs#L201.
182+
183+
Example: (This one happens to compile in the current nightly but more practical examples are
184+
already rejected with confusing errors.)
185+
```rust
186+
type Opaque<'x> = impl Sized + 'x;
187+
188+
fn test<'a>() -> Opaque<'a> {
189+
let _ = || {
190+
// `'a` is external to the closure
191+
let _: Opaque<'a> = ();
192+
//~^ Should be an error!
193+
};
194+
()
195+
}
196+
```
197+
198+
**Motivation:**
199+
In closure bodies, external lifetimes, although being categorized as "universal" lifetimes,
200+
behave more like existential lifetimes in that the relations between them are not known ahead of
201+
time, instead their values are inferred just like existential lifetimes and the requirements are
202+
propagated back to the parent fn. This breaks the member constraints algorithm as described above:
203+
> In order for the algorithm to pick the right choice, the complete set of “outlives” relationships
204+
between the choice regions ['static, '?1] must already be known before doing the region inference
205+
206+
Here is an example that details how :
207+
208+
```rust
209+
type Opaque<'x, 'y> = impl Sized;
210+
211+
//
212+
fn test<'a, 'b>(s: &'a str) -> impl FnOnce() -> Opaque<'a, 'b> {
213+
move || { s }
214+
//~^ ERROR hidden type for `Opaque<'_, '_>` captures lifetime that does not appear in bounds
215+
}
216+
217+
// The above closure body is desugared into something like:
218+
fn test::{closure#0}(_upvar: &'?8 str) -> Opaque<'?6, '?7> {
219+
return _upvar
220+
}
221+
222+
// where `['?8, '?6, ?7] are universal lifetimes *external* to the closure.
223+
// There are no known relations between them *inside* the closure.
224+
// But in the parent fn it is known that `'?6: '?8`.
225+
//
226+
// When encountering an opaque definition `Opaque<'?6, '?7> := &'8 str`,
227+
// The member constraints algotithm does not know enough to safely make `?8 = '?6`.
228+
// For this reason, it errors with a sensible message:
229+
// "hidden type captures lifetime that does not appear in bounds".
230+
```
231+
232+
Without this restrictions error messages are consfusing and, more impotantly, there is a risk that
233+
we accept code the we would likely break in the future because member constraints are super broken
234+
in closures.
235+
236+
**Output types:**
237+
I believe the most common scenario where this causes issues in real-world code is with
238+
closure/async-block output types. It is worth noting that there is a discrepancy betweeen closures
239+
and async blocks that further demonstrates this issue and is attributed to the
240+
[hack of `replace_opaque_types_with_inference_vars`][source-replace-opaques],
241+
which is applied to futures only.
242+
243+
[source-replace-opaques]: https://github.com/rust-lang/rust/blob/9cf18e98f82d85fa41141391d54485b8747da46f/compiler/rustc_hir_typeck/src/closure.rs#L743
244+
245+
```rust
246+
type Opaque<'x> = impl Sized + 'x;
247+
fn test<'a>() -> impl FnOnce() -> Opaque<'a> {
248+
// Output type of the closure is Opaque<'a>
249+
// -> hidden type definition happens *inside* the closure
250+
// -> rejected.
251+
move || {}
252+
//~^ ERROR expected generic lifetime parameter, found `'_`
253+
}
254+
```
255+
```rust
256+
use std::future::Future;
257+
type Opaque<'x> = impl Sized + 'x;
258+
fn test<'a>() -> impl Future<Output = Opaque<'a>> {
259+
// Output type of the async block is unit `()`
260+
// -> hidden type definition happens in the parent fn
261+
// -> accepted.
262+
async move {}
263+
}
264+
```

0 commit comments

Comments
 (0)