Skip to content

Commit d81c6cc

Browse files
committed
Deemphasize TAIT a bit
In the design meeting for the 2024 lifetime capture rules, we presented TAIT as being the intended solution to the problem of overcapturing. By "intended", we simply meant that it is known to be a full solution, and since it has accepted RFCs and a consensus to stabilize it in some form, it's more *intended* than other solutions lacking those things. However, as applied to the rules in this RFC, the only thing that's important is that *some* solution is stabilized that allows for existing code to continue to be expressed in some manner. The RFC had already said this (e.g., "...is contingent on the stabilization of TAIT or some other solution..."), but it was a bit obscured by the emphasis on TAIT. Let's deemphasize TAIT and describe the landscape of solutions more fully so as to keep the focus on the new capture rules. We'll move the discussion of TAIT out of the normative section entirely and purge the word "intended" from the document. (Thanks to @tmandry for pointing this out.)
1 parent 832291f commit d81c6cc

File tree

1 file changed

+88
-45
lines changed

1 file changed

+88
-45
lines changed

text/3498-lifetime-capture-rules-2024.md

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,15 @@ fn bar(x: ()) -> impl Sized + 'static {
210210

211211
In Rust 2021, lifetimes within type parameters are automatically captured in RPIT opaque types, and both lifetime parameters and lifetimes within type parameters are automatically captured in `async fn` opaque types. Additionally capturing lifetime parameters in RPIT opaque types may make this problem somewhat worse.
212212

213-
The intended solution for this problem is type alias `impl Trait` (TAIT). The stabilization of the 2024 lifetime capture rules in this RFC is contingent on the stabilization of TAIT or some other solution for precise capturing that will allow all code that is allowed under Rust 2021 to be expressed, in some cases with syntactic changes, in Rust 2024.
213+
There are a number of possible solutions to this problem. One appealing partial solution is to more fully implement the rules specified in [RFC 1214][]. This would allow type and lifetime parameters that do not outlive a specified bound to mostly act as if they were not captured. See [Appendix G][] for a full discussion of this.
214214

215-
For an example of how TAIT is intended to be used to fix overcapturing, see the section titled "[TAIT as the solution to overcapturing]".
215+
Another solution would be to add syntax for precisely specifying which type and lifetime parameters to capture. One proposal for this syntax is described in [Appendix F][].
216+
217+
Type alias `impl Trait` (TAIT) is another solution. It has accepted RFCs (see [RFC 2515][], [RFC 2071][]), it's implemented and actively maintained in nightly Rust, and there is a consensus to stabilize it in some form. The stabilization of TAIT would allow all currently accepted code to continue to be expressed with precisely the same semantics. See [Appendix I][] for further details on how TAIT can be used to precisely control the capturing of type and lifetime parameters.
218+
219+
The stabilization of the 2024 lifetime capture rules in this RFC is contingent on the stabilization of some solution for precise capturing that will allow all code that is allowed under Rust 2021 to be expressed, in some cases with syntactic changes, in Rust 2024.
220+
221+
[RFC 1214]: https://github.com/rust-lang/rfcs/blob/master/text/1214-projections-lifetimes-and-wf.md
216222

217223
## Summary of problems
218224

@@ -306,47 +312,11 @@ Note that support for higher kinded lifetime bounds is not required by this RFC
306312

307313
[#104288]: https://github.com/rust-lang/rust/issues/104288
308314

309-
## TAIT as the solution to overcapturing
310-
311-
[TAIT as the solution to overcapturing]: #tait-as-the-solution-to-overcapturing
312-
313-
As we described above, sometimes the capture rules result in unwanted type and lifetime parameters being captured. This happens in Rust 2021 due to the RPIT rules for capturing lifetimes from all in-scope type parameters and the `async fn` rules for capturing all in-scope type and lifetime parameters. Under this RFC, in Rust 2024, lifetime parameters could also be overcaptured by RPIT.
314-
315-
The intended solution to this is type alias `impl Trait` (TAIT). It works as follows. Consider this overcaptures scenario in Rust 2024:
316-
317-
```rust
318-
fn foo<'a, T>(_: &'a (), _: T) -> impl Sized { () }
319-
// ^^^^^^^^^^
320-
// The returned opaque type captures `'a` and `T`
321-
// but the hidden type does not use either.
322-
323-
fn bar<'a, 'b>(x: &'a (), y: &'b ()) {
324-
fn is_static<T: 'static>(_: T) {}
325-
is_static(foo(x, y));
326-
// ^^^^^^^^^
327-
// Error: `foo` captures `'a` and `'b`.
328-
}
329-
```
330-
331-
In the above code, we want to rely on the fact that `foo` does not actually use any lifetimes in the returned hidden type. We can't do that using RPIT because there's no way to prevent the opaque type from capturing too much. However, we can use TAIT to solve this problem elegantly as follows:
332-
333-
```rust
334-
#![feature(type_alias_impl_trait)]
335-
336-
type FooRet = impl Sized;
337-
fn foo<'a, T>(_: &'a (), _: T) -> FooRet { () }
338-
// ^^^^^^
339-
// The returned opaque type does NOT capture `'a` or `T`.
340-
341-
fn bar<'a, 'b>(x: &'a (), y: &'b ()) {
342-
fn is_static<T: 'static>(_: T) {}
343-
is_static(foo(x, y)); // OK.
344-
}
345-
```
315+
## Overcapturing
346316

347-
The type alias `FooRet` has no generic parameters, so none are captured in the opaque type. It's always possible to desugar an RPIT opaque type into a TAIT opaque type that expresses precisely which generic parameters to capture.
317+
Sometimes the capture rules result in unwanted type and lifetime parameters being captured. This happens in Rust 2021 due to the RPIT rules for capturing lifetimes from all in-scope type parameters and the `async fn` rules for capturing all in-scope type and lifetime parameters. Under this RFC, in Rust 2024, lifetime parameters could also be overcaptured by RPIT.
348318

349-
The stabilization of the 2024 lifetime capture rules in this RFC is contingent on the stabilization of TAIT or some other solution for precise capturing that will allow all code that is allowed under Rust 2021 to be expressed, in some cases with syntactic changes, in Rust 2024.
319+
The stabilization of the 2024 lifetime capture rules in this RFC is contingent on the stabilization of some solution for precise capturing that will allow all code that is allowed under Rust 2021 to be expressed, in some cases with syntactic changes, in Rust 2024.
350320

351321
## Type alias `impl Trait` (TAIT)
352322

@@ -612,7 +582,9 @@ fn test<'t, 'x>(t: &'t (), x: &'x ()) {
612582

613583
# Appendix F: Future possibility: Precise capturing syntax
614584

615-
Under this RFC, TAIT is the intended solution to avoid capturing type and lifetime parameters that should not be captured. If this comes up too often in the future, we may want to consider adding new syntax to `impl Trait` to allow for precise capturing. One proposal for that would look like this:
585+
[Appendix F]: #appendix-f-future-possibility-precise-capturing-syntax
586+
587+
If other solutions for precise capturing of type and lifetime parameters turn out to be unergonomic or needed too often, we may want to consider adding new syntax to `impl Trait` to allow for precise capturing. One proposal for that would look like this:
616588

617589
```rust
618590
fn foo<'x, 'y, T, U>() -> impl<'x, T> Sized { todo!() }
@@ -623,7 +595,9 @@ fn foo<'x, 'y, T, U>() -> impl<'x, T> Sized { todo!() }
623595

624596
# Appendix G: Future possibility: Inferred precise capturing
625597

626-
Under this RFC, TAIT is the intended solution to avoid capturing type and lifetime parameters that should not be captured. However, when an outlives bound is stated for the opaque type, we can use that bound to allow code to compile that does not currently. Consider:
598+
[Appendix G]: #appendix-g-future-possibility-inferred-precise-capturing
599+
600+
When an outlives bound is stated for the opaque type, we can use that bound to allow code to compile that does not currently. Consider:
627601

628602
```rust
629603
fn capture<'o, T>(_: T) -> impl Send + 'o {}
@@ -674,7 +648,26 @@ fn test_outlives<'o, T: PhantomCapture>(x: T) {
674648
}
675649
```
676650

677-
Future work may relax this current limitation of the compiler. The result of such an improvement would be that, when an outlives bound is specified for the opaque type, any type or lifetime parameters that the compiler could prove to not outlive that bound would act as if they were not captured by the opaque type.
651+
Future work may relax this current limitation of the compiler by more fully implementing the rules of [RFC 1214][] (see, e.g., [#116733][]). Fixing this completely is believed to require support in the compiler for existential lifetimes (see [#60670][]).
652+
653+
The end result of these improvements would be that, when an outlives bound is specified for the opaque type, any type or lifetime parameters that the compiler could prove to not outlive that bound would mostly act as if it were not captured by the opaque type.
654+
655+
This would not be quite the same as those type and lifetime parameters not actually being captured. By checking type equality between opaque types where different captured type or lifetime parameters have been substituted, one could tell the difference.
656+
657+
Still, this improvement would allow for solving many cases of overcapturing elegantly. Consider this transformation:
658+
659+
```rust
660+
fn callee<P1, .., Pn>(..) -> impl Trait { .. }
661+
//-------------------------------------------------------------
662+
fn callee<'o, P1: 'o, .., Pn: 'o>(..) -> impl Trait + 'o { .. }
663+
```
664+
665+
Using this transformation (which is described more fully in [Appendix H][]), we can add a specified outlives bound to an RPIT opaque type without changing the effective proof requirements on either the caller or the callee. We can then drop the `Pi: 'o` outlives bound from any type or lifetime parameter that we would like to act as if it were not captured.
666+
667+
This comes at the cost of adding an extra early-bound lifetime parameter in the general case. Adding that lifetime parameter may require changing the externally visible API of the function. However, for the common case of adding a `+ 'static` bound, or for any other case where an existing lifetime parameter suffices to specify the needed bounds, this is not a problem.
668+
669+
[#60670]: https://github.com/rust-lang/rust/issues/60670
670+
[#116733]: https://github.com/rust-lang/rust/pull/116733
678671

679672
# Appendix H: Examples of outlives rules on opaque types
680673

@@ -773,4 +766,54 @@ In the first example, to prove that the opaque type outlives `'short`, the *call
773766

774767
(Obviously, the caller then still needs to prove the outlives relationships necessary to satisfy the other specified bounds in the signature of `callee`.)
775768

776-
That is, at the cost of an extra early-bound lifetime parameter in the signature of the callee, we can always express an RPIT without a specified outlives bound as an RPIT with a specified outlives bound in a way that does not change the requirements on the caller or the callee. We do this by transforming a signature of the form `fn callee<P1, .., Pn>(..) -> impl Trait` to a signature of the form `fn callee<'o, P1: 'o, .., Pn: 'o>(..) -> impl Trait + 'o`.
769+
That is, at the cost of an extra early-bound lifetime parameter in the signature of the callee, we can always express an RPIT without a specified outlives bound as an RPIT with a specified outlives bound in a way that does not change the requirements on the caller or the callee. We do this by applying the following transformation:
770+
771+
```rust
772+
fn callee<P1, .., Pn>(..) -> impl Trait { .. }
773+
//-------------------------------------------------------------
774+
fn callee<'o, P1: 'o, .., Pn: 'o>(..) -> impl Trait + 'o { .. }
775+
```
776+
777+
One application of this transformation to solve problems created by overcapturing is described in [Appendix G][].
778+
779+
# Appendix I: Precise capturing with TAIT
780+
781+
[Appendix I]: #appendix-i-precise-capturing-with-tait
782+
783+
Sometimes the capture rules result in unwanted type and lifetime parameters being captured. This happens in Rust 2021 due to the RPIT rules for capturing lifetimes from all in-scope type parameters and the `async fn` rules for capturing all in-scope type and lifetime parameters. Under this RFC, in Rust 2024, lifetime parameters could also be overcaptured by RPIT.
784+
785+
Type alias `impl Trait` (TAIT) provides a precise solution. It works as follows. Consider this overcaptures scenario in Rust 2024:
786+
787+
```rust
788+
fn foo<'a, T>(_: &'a (), _: T) -> impl Sized { () }
789+
// ^^^^^^^^^^
790+
// The returned opaque type captures `'a` and `T`
791+
// but the hidden type does not use either.
792+
793+
fn bar<'a, 'b>(x: &'a (), y: &'b ()) {
794+
fn is_static<T: 'static>(_: T) {}
795+
is_static(foo(x, y));
796+
// ^^^^^^^^^
797+
// Error: `foo` captures `'a` and `'b`.
798+
}
799+
```
800+
801+
In the above code, we want to rely on the fact that `foo` does not actually use any lifetimes in the returned hidden type. We can't do that using RPIT because there's no way to prevent the opaque type from capturing too much. However, we can use TAIT to solve this problem elegantly as follows:
802+
803+
```rust
804+
#![feature(type_alias_impl_trait)]
805+
806+
type FooRet = impl Sized;
807+
fn foo<'a, T>(_: &'a (), _: T) -> FooRet { () }
808+
// ^^^^^^
809+
// The returned opaque type does NOT capture `'a` or `T`.
810+
811+
fn bar<'a, 'b>(x: &'a (), y: &'b ()) {
812+
fn is_static<T: 'static>(_: T) {}
813+
is_static(foo(x, y)); // OK.
814+
}
815+
```
816+
817+
The type alias `FooRet` has no generic parameters, so none are captured in the opaque type. It's always possible to desugar an RPIT opaque type into a TAIT opaque type that expresses precisely which generic parameters to capture.
818+
819+
The stabilization of the 2024 lifetime capture rules in this RFC is contingent on the stabilization of some solution for precise capturing that will allow all code that is allowed under Rust 2021 to be expressed, in some cases with syntactic changes, in Rust 2024.

0 commit comments

Comments
 (0)