owned this note
owned this note
Published
Linked with GitHub
---
title: ITE meeting 2023-10-30
tags: impl-trait-everywhere, triage-meeting, minutes
date: 2023-10-30
discussion: https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/ITE.20triage.20meeting.202023-10-30
url: https://hackmd.io/TsRfxTpfQGKy_zXcQm8KDA
---
# ITE meeting agenda
- Meeting date: 2023-10-30
## Attendance
- People: TC, CE, tmandry
## Meeting roles
- Minutes: TC
## Resolving #107645
Here's the context. The T-types meetup on 2023-10-11 resulted in a consensus proposal from T-types on how to move forward on TAIT.
However, this proposal builds on the [last proposal](https://hackmd.io/oTC4J-2XRnukxC7lSq_PVA) which was discussed in a T-lang [design meeting](https://hackmd.io/IVFExd28TZWm6iyNIq66PA) on 2023-05-31. That meeting resulted in a 2023-06-01 [proposed FCP](https://github.com/rust-lang/rust/issues/107645#issuecomment-1571789814) on [#107645][] to move forward. On that proposed FCP, nikomatsakis, pnkfelix, scottmcm, and tmandry have checked boxes, and tmandry raised two concerns which we'll discuss here.
Before the [T-types proposal](https://hackmd.io/qiy4_I3WRYyhpYvjbYBrew) is proposed for a new T-lang design meeting, we should resolve if possible [#107645][]. The T-types proposal will itself require a full design meeting. If we can resolve [#107645][] along the lines of the original FCP, then that design meeting can happen directly. Otherwise, we'll probably need two design meetings: one to bring people back up to speed on the [#107645][] issues to potentially affect that consensus, and one on the T-types proposed restrictions.
In the remainder of this document, we'll discuss the open concerns on [#107645][] and the motivations that underlie the original proposal.
[#107645]: https://github.com/rust-lang/rust/issues/107645
### Constraining through encapsulation
errs: Could also call it "poking thru struct fields"
What we called "constraining through encapsulation" is the idea that the signature rule is passed if the *opaque type* appears within any type mentioned in the signature rather than requiring that the *type alias* be mentioned in the signature directly.
#### Motivation
The claim we make in support of the signature restriction is that most anticipated uses of TAIT will pass the signature restriction without the user having to take special measures. If we disallow constraining this encapsulation, this becomes meaningfully less true. Consider trying to write a `new` constructor for a type:
```rust
#![feature(type_alias_impl_trait)]
use core::future::Future;
type JobFut = impl Future<Output = u64>;
struct Job {
id: u64,
fut: JobFut,
}
impl Job {
fn new(id: u64) -> Self {
// ~^ ?? ERROR item constrains opaque type that is not in its signature
Job { id: id, fut: async move { id } }
}
}
```
We believe this will be a common pattern, and it would be a shame if this didn't work. The common `Builder` pattern would also be affected by this.
Some people have been vocally concerned that the signature restriction might not invisibly embody enough use cases, that people might have to think about it too often, and that they may need to refactor their code too often and too drastically to satisfy it. We disagree with this assessment. After much analysis, we believe that the signature restriction does quietly capture most use cases, and that any required code refactorings are not severe and still result in reasonable code (often using the `Builder` pattern).
However, if we were to restrict constraining through encapsulation, that would suddenly become less true and those concerns may take on more weight.
#### Concerns in [#107645][]
On [#107645][], tmandry raised the following points, which we'll address in order.
#### No loss of expressiveness
> [Paraphrased]: Requiring the type alias to be mentioned has no loss of expressiveness as the code could always be refactored in such a way that some function directly returns the opaque type named directly by the type alias.
This is correct. Such a refactoring is always possible and no fundamental expressiveness is lost, as far as we are aware.
#### Use of grep
> [Paraphrased]: Such a refactoring would preserve the property that one would only need to grep for the type alias to find defining uses.
Clearly such a refactoring would indeed preserve this property. There are basically two lines of argument here.
One is that `impl Trait` is meant to be similar to `dyn Trait`, and in this respect, `dyn Trait` is exactly the same. When dealing with a `dyn Trait` opaque type, sometimes it would be helpful to find every place that opaque type might be filled in with something of a concrete type (e.g. so you could know what impls should be checked to verify behavior). But when a type alias with a `dyn Trait` is composed in a struct, we don't require the type alias to appear in the signature.
With `dyn Trait` we're OK with people having to rely on IDEs or other forms of analysis. The same argument could be applied here.
The second point here is that `grep` is not needed to find all defining uses, even without advanced tooling such as `rust-analyzer`. Anyone dealing in Rust code presumably has access to the compiler, and it's easy to ask the compiler to locate all defining uses.
E.g., let's say that `JobFut` is a type alias with an `impl Trait` opaque type. To find all defining uses, we can simply write this:
```rust
// Let's find all constraining uses of `JobFut`
// by adding a bogus constraint.
fn nop() -> JobFut { async {} }
```
Then `cargo check` will tell us:
```
error: concrete type differs from previous defining opaque type use
--> src/lib.rs:16:13
|
16 | tx.send(Job(0u64, async { todo!() })).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `[async block@src/lib.rs:12:22: 12:30]`, got `[async block@src/lib.rs:16:23: 16:40]`
|
error: concrete type differs from previous defining opaque type use
... other locations of defining uses
```
#### Difficult to understand code
> [Paraphrased]: It could lead to code that's difficult to understand as a potentially large body of code could define an opaque type.
Again, the situation here is very analogous to `dyn Trait`. Arguably `impl Trait` by its nature results is code that's simpler than `dyn Trait` because with `impl Trait` there can be by construction only one concrete type.
However, if we were to find this to be a problem, we propose that the solution is to lint about this rather than to limit this at the level of the language.
As we've seen with the stabilization of AFIT, linting is a powerful mechanism that we do have at our disposal. Unlike changes to the language, linting is lightweight in the sense that we can always change our mind and respond to the evidence of what is or is not a problem.
#### Motivation reprise
The bottom line is that while we have sympathy for the concerns above, we're very motivated by ensuring the design makes this code work:
```rust
#![feature(type_alias_impl_trait)]
use core::future::Future;
type JobFut = impl Future<Output = u64>;
struct Job {
id: u64,
fut: JobFut,
}
impl Job {
fn new(id: u64) -> Self {
// ~^ ?? ERROR item constrains opaque type that is not in its signature
Job { id: id, fut: async move { id } }
}
}
```
### Nested functions
On this point, tmandry raised the following point:
> The restriction for nested inner functions feels inconsistent with making inner modules unrestricted, so I think we should resolve the inconsistency or provide more of a rationale before stabilizing.
The proposal allows:
```rust
mod a {
type Foo = impl Sized;
mod b {
fn define() -> super::Foo {}
}
}
```
And it allows:
```rust
mod a {
type Foo = impl Sized;
fn b() -> Foo {
fn define() -> Foo {}
define()
}
}
```
But it rejects:
```rust
mod a {
type Foo = impl Sized;
fn b() {
fn define() -> Foo {}
_ = define();
}
}
```
This is a natural result of applying the signature rule recursively. It could seem odd if items within functions could ignore the signature restriction. More importantly, perhaps, it could raise concern from the creators of tooling such as `rust-analyzer` who want to rely on the signature restriction to limit which function bodies need to be parsed.
If we were to go the other way, and force the opaque type to be defined at the same scoping level at which it is introduced, then we would reject code like this:
```rust
mod a {
type Foo = impl Sized;
mod b {
fn define() -> super::Foo {}
}
}
```
But that would mean we should also reject this code:
```rust
mod a {
type Foo = impl Sized;
fn b() -> Foo { // `b` is non-defining.
fn define() -> Foo {}
define()
}
}
```
That might seem a rather arbitrary restriction.
Again, if this proved to be a problem in practice, we could certainly use linting to push people away from relying on deep or wide defining scopes.
---
However, we all agree that the restriction against nested items being able to define an opaque type is somewhat arbitrary, and we'd be happy to remove it if possible. In combination with the T-types proposal, the restriction becomes much more severe. In that context, we should see again about whether it is possible to simply lift this restriction.
## Summary
We propose that the concerns on [#107645][] be resolved so that issue can be into FCP and clear the way to present the T-types proposal to T-lang.
To whatever degree that the concerns raised may in fact turn out to be problems, those problems can be addressed in a satisfactory manner through linting.
Conversely, restricting these capabilities could serve to undermine elements of the design rationale.
## Future work: Constraining outside of the defining scope
This proposal is forward compatible with future work that would allow the hidden type to be constrained within the same crate but outside of the defining scope using a new syntax. E.g.:
```rust
#![feature(type_alias_impl_trait)]
#![feature(todo_define_tait_anywhere_in_crate)]
use taits::*;
mod taits {
type Tait<T> = impl Sized;
}
fn define<T>()
where
constrains(Tait<T>)
{}
```
One useful property of such future work is that those who wish to not rely on the signature restriction and wish to always explicitly annotate which functions may constrain the hidden type of some opaque may do so simply by placing their TAITs in a submodule as above.
## References
- [2023-10-30 Attempt to resolve #107645](https://hackmd.io/TsRfxTpfQGKy_zXcQm8KDA) (this document)
- [2023-10-26 Description of T-types proposal](https://hackmd.io/qiy4_I3WRYyhpYvjbYBrew)
- [2023-10-11 T-types TAIT session minutes](https://hackmd.io/QOsEaEJtQK-XDS_xN4UyQA)
- [2023-09-13 RPITIT stabilization](https://github.com/rust-lang/rust/pull/115822)
- [2023-07-26 Lifetime capture rules 2024](https://github.com/rust-lang/rfcs/pull/3498) ([design meeting](https://hackmd.io/sFaSIMJOQcuwCdnUvCxtuQ)) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-07-26))
- [2023-07-11 T-lang triage diffs on TAIT](https://hackmd.io/_eMqgF3JQgGEN4Y6C9C1pg#Diffs-on-TAIT-TC)
- [2023-06-29 TAIT must be constrained if in signature PR](https://github.com/rust-lang/rust/pull/113169)
- [2023-06-29 Oli/lcnr meeting on TAIT](https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/lcnr.20oli.20meeting/near/370710606)
- [2023-06-29 TAIT mini-design meeting](https://hackmd.io/r1oqcjrzTAK5e_T1IOXeXg) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/TAIT.20mini-design.20meeting.202023-06-29))
- [2023-06-27 T-lang triage atempt to revise nested inner functions restriction](https://hackmd.io/hTUmwMrbSSqN1eU2k90Iwg#TAIT-nested-inner-functions-restriction-take-2-TC)
- [2023-06-13 TAIT tracking issue proposed stabilization FCP canceled](https://github.com/rust-lang/rust/issues/63063#issuecomment-1588994092)
- [2023-06-12 T-types TAIT in new trait solver document](https://hackmd.io/llGcGMR7SvCP1C1MulcDQw) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/326132-t-types.2Fmeetings/topic/2023-06-12.20TAIT.20in.20new.20solver/near/365570768))
- [2023-06-06 lcnr resolves concern about allowing WCs in signature restriction](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-05-31.20TAITs/near/363984835)
- [2023-06-01 TAIT defining scope options proposed FCP](https://github.com/rust-lang/rust/issues/107645#issuecomment-1571789814)
- [2023-05-31 T-lang TAIT design meeting](https://hackmd.io/IVFExd28TZWm6iyNIq66PA) ([discussion](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/design.20meeting.202023-05-31.20TAITs))
- [2023-05-31 TAIT draft stabilization report](https://hackmd.io/oTC4J-2XRnukxC7lSq_PVA) (not updated with T-types proposal)
- [2023-03-20 lcnr update on new trait solver concern](https://github.com/rust-lang/rust/issues/63063#issuecomment-1476196975)
- [2023-02-06 lcnr concern over new trait solver](https://github.com/rust-lang/rust/issues/63063#issuecomment-1418741032)
- [2023-01-17 TAIT tracking issue concern over defining scope](https://github.com/rust-lang/rust/issues/63063#issuecomment-1386064436)
- [2022-12-24 TAIT tracking issue concern over updating reference](https://github.com/rust-lang/rust/issues/63063#issuecomment-1364525286)
- [2022-12-20 proposed FCP merge of TAIT stabilization](https://github.com/rust-lang/rust/issues/63063#issuecomment-1360043060)
- [2022-12-16 TAIT stabilization report](https://github.com/rust-lang/rust/issues/63063#issuecomment-1354392317)
- [2019-06-28 TAIT tracking issue](https://github.com/rust-lang/rust/issues/63063)
- [2018-08-05 RFC 2515 - Permit impl Trait in type aliases](https://github.com/rust-lang/rfcs/pull/2515)
- [2017-07-20 RFC 2071 - Named existentials and impl Trait variable declarations](https://github.com/rust-lang/rfcs/pull/2071)
- [2017-03-15 RFC 1951 - Finalize syntax and parameter scoping for impl Trait, while expanding it to arguments](https://github.com/rust-lang/rfcs/pull/1951)
- [2016-03-01 RFC 1522 - Minimal impl Trait](https://github.com/rust-lang/rfcs/pull/1522)
- [impl Trait initiative repository](https://rust-lang.github.io/impl-trait-initiative/)
- [TAIT project tracking board](https://github.com/orgs/rust-lang/projects/22/views/1)
---
# Questions
## Constraining rules
Rules:
1. Any function within a defining scope can constrain iff it has the TAIT (or some ADT containing the TAIT) in its signature, including where clauses
2. If it can constrain, it *must* constrain
3. Only one function can constrain.
(1) must remain forever, (2) can be removed once the new trait solver is in place; (3) is not load bearing.
(...superseded by the next section.)
## From the T-types meeting: Proposal :zero: (preferred)
- Forever:
- A function must have the TAIT (or a type that contains it) in the signature.
- or some defines syntax in the future
- For now (we never expect to relax):
- TAITs can only be defined once modulo regions.
- For now (can be relaxed later):
- A function that mentions the TAIT (or a type that contains it) in the signature, it must define it.
- Because of lazy norm and how the new trait solver is more complete, this is an area of difference between the old and new solver. It would be easy to break things here. And the old trait solver is underspecified here. (Oli: It is fixable in the old solver.) So we're saving space here.
- Only one function in the defining scope can mention the TAIT (or a type that contains it) in the signature.
- Can create a dedicated diagnostic for this case, avoiding all cycle errors and other hard to diagnose issues for users.
- This is the most arbitrary. We have the machinery to allow this. But it prevents people from writing functions that are passthrough. It allows us to write earlier and better diagnostics. But this is an artificial restriction we could lift easily. We could put this behind a separate feature gate.
- Error if projection in signature (except one from the same impl, ITIAT/ATPIT) inside the defining scope normalizes to include a TAIT.
- Saves space for making opaque_types_defined_by query smarter.
- Properties:
- All cycle errors are *real* cycle errors in the new solver.
- Changes that allow more items to define the TAIT to the signature rule would be breaking changes.
```rust
trait Mirror {
type Assoc;
}
impl<T> Mirror for T {
type Assoc = T;
}
fn constrains() -> <Tait as Mirror>::Assoc {}
```
The wrap/unwrap pattern:
```rust
#![feature(type_alias_impl_trait)]
type Tait = impl Sized;
fn wrap(x: ()) -> Tait { x }
fn unwrap(x: Tait) -> () { x }
```
## Opt-in `#[defines]`
One could actually implement opt-in `#[defines(..)]` as a proc macro under the proposed system within the defining scope.
```rust
#[defines(Foo)]
fn foo() { let _: Foo = (); }
// -----------------------------------------
fn foo() where Foo: Any { let _: Foo = (); }
```
## Decision framework for signature restriction vs `#[defines]`
We can accept any of the following today within the defining scope:
* Signature restriction only (=> A, C)
* `#[defines]` only (=> B)
* Signature restriction _and_ `#[defines]` (=> A, B, C)
* Annoying: Every function that passes signature restriction must have `#[defines]`
* Otherwise, when we relax the `defines` in defining scope (A), would cause inference problems possibly
Future possibilities:
* A) Signature restriction (within defining scope) _or_ `#[defines]` (in crate)
* B) `#[defines]` iff constrains TAIT (in crate)
* C) Lint against defining functions without `#[defines(..)]` everywhere
* (same as A, plus lint).
## Concerns with implementing `#[defines]` today
There are technical restrictions to this today; name resolution doesn't happen inside attributes.
## Inputs into trait solver
Today:
* Whether you are in the defining scope (descendant mod).
Tomorrow:
* Whether you are actually allowed to constrain (decsendant mod + signature restriction).