|
| 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