Skip to content

Commit 3d75a78

Browse files
authored
Merge pull request #41 from nikomatsakis/wf-checks-type-aliases
add minutes from design meeting on WF checks for type aliases
2 parents f977c4f + 5b95245 commit 3d75a78

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

design-meeting-minutes/2020-07-29.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Discuss WF checks and type aliases
2+
3+
* Meeting issue: https://github.com/rust-lang/lang-team/issues/25
4+
* [Watch the recording](https://youtu.be/tIBZYQSA_eM)
5+
6+
## Context
7+
8+
The way we implement type aliases goes way back. We basically eagerly expand them when 'convering' from the syntax tree into the compiler's internal representation of types. When you have e.g. `type MyAlias = u32; fn foo(x: MyAlias)`, the type-checker just sees `fn foo(x: u32)`, in effect, and we ignore the `type MyAlias = u32`.
9+
10+
We also permit types that are not well-formed. These only report an error if they are used:
11+
12+
```rust
13+
struct MyType<T: Debug> { }
14+
type MyAlias = MyType<dyn Display>; // dyn is not Sized, Display is not Debug
15+
16+
fn foo(x: MyAlias) { }
17+
// seen as `fn foo(x: MyType<dyn Display>)`
18+
```
19+
20+
One consequence is that we allow type aliases with both too many and too few where clauses:
21+
22+
```rust
23+
struct MyType<T: Debug> { }
24+
25+
type JustRightAlias<T: Debug> = MyType<T>; // JustRightAlias<X> will error if X: Debug is not true
26+
type TooFewAlias<T> = MyType<T>;
27+
type TooManyAlias<T: Debug + Display> = MyType<T>; // TooManyAlias<Vec<u32>> is not an error
28+
```
29+
30+
We do not report errors it `TooManyAlias<Vec<u32>>` is used, even though `Vec<u32>: Display` is not true.
31+
32+
One way we do look at those where clauses:
33+
34+
```rust
35+
type IteratorItem<T> = T::Item; // error
36+
type IteratorItem<T> = <T as Iterator>::Item;
37+
type IteratorItem<T: Iterator> = T::Item;
38+
```
39+
40+
We also give warnings if you do use bounds.
41+
42+
## What behavior do we ultimately expect?
43+
44+
One important thing for us to decide is what behavior we ultimately expect here, and perhaps the interaction with implied bounds.
45+
46+
### Naive behavior
47+
48+
I think what I would most naively expect is that `type Foo<T> where WC = U` is legal if
49+
50+
```rust
51+
struct Foo<T> where WC {
52+
f: U
53+
}
54+
```
55+
56+
is legal (not including any struct-specific requirements around `Sized`).
57+
58+
### Implied
59+
60+
But Niko has usability concerns related to implied bounds -- will this result in a bunch of copying of where clauses around?
61+
62+
```rust
63+
struct Foo<T: Debug> { }
64+
impl Foo<T> {
65+
// can assume T: Debug is true in here
66+
}
67+
```
68+
69+
```rust
70+
struct SomeStruct<T: Debug> = ...;
71+
72+
// feels like annoying boilerplate for `T`:
73+
type VecSomeStruct<T> = Vec<SomeStruct<T>>
74+
```
75+
76+
Another implied bounds like question:
77+
78+
```rust
79+
fn foo<T>(x: VecSomeStruct<T>) {
80+
// can I assume T: Debug, just like I would if `Vec<SomeStruct<T>>`? Probably yes?
81+
}
82+
```
83+
84+
### Rationalizing the current behavior
85+
86+
I've considered that we might "rationalize" the current behavior if we had a notion of implied bounds where a type-alias *assumes* the RHS is well-formed. For example, `type TooFewAlias<T> = MyType<T>` would be sort of "short for"
87+
88+
```rust
89+
type TooFewAlias<T> where WellFormed(MyType<T>) = MyType<T>
90+
```
91+
92+
(this is not legal syntax, of course).
93+
94+
This is probably similar to what we would wind up doing for WF in `fn`, since we have some similar backwards compatibility constraints there (e.g., `for<'a> fn(Foo<'a>)` is always considered WF today, even if you have `struct Foo<'a: 'static> { }`).
95+
96+
We can also change the default for type parameters to be `?Sized` here, which is key for examples like `type Foo<T> = Box<T>`.
97+
98+
**Catch.** However, this does not handle the fact that `TooManyAlias` does not enforce its "extra bounds".
99+
100+
## Experiments conducted
101+
102+
eddyb made a number of PRs around the time of the Rust 2018 edition doing various experiments. They were looking at ways to enforce bounds at the definition site. If we could show that there were "exactly the right set of bounds" for type aliases, and that the RHS is well-formed, then we have a reasonably complete check.
103+
104+
Some interesting examples:
105+
106+
* `type Foo<T> = Box<T>` -- too many constraints, but common
107+
* `type Foo<T: ?Sized>`
108+
* if we had the implied bounds approach, we could also make `T: ?Sized` the default in type aliases
109+
110+
## estebank's PR
111+
112+
Error at the declaration site if:
113+
114+
```
115+
type Foo = Vec<dyn Debug>;
116+
```
117+
118+
## Observation:
119+
120+
If we wanted the "implied bounds" solution, then checking for "too many bounds" at the declaration site would be a conservative version of this (ignoring Sized bounds). eddyb ran this test in https://github.com/rust-lang/rust/pull/69741 and found 60-odd regressions, including some impact on Diesel.
121+
122+
## Questions
123+
124+
* What direction do we want?
125+
* "Naive approach" or "implied bounds" approach
126+
* Note: Implied bounds says that impls/fns get to assume their "arguments" are valid, this is a bit different potentially
127+
* How much do we care about pushing on this right now, versus later? (e.g., when the chalk work has landed and implied bounds machinery can be better evaluated)
128+
129+
Josh:
130+
131+
* I see the appeal of the "implied bounds" approach, but I think that if you write *any* bounds, you should write both "the exact correct" set of bounds, and not too many / too few
132+
* Suggests a possible "migration strategy" we might enforce rules only in cases where people wrote *some* explicit bound (and not e.g. `type Foo<T>`).
133+
* We should not warn if you specify no bounds at all; that'd be consistent with implied bounds.
134+
* We should do the same for functions: you shouldn't have to restate all the bounds of a type in order to use that type.
135+
* We should warn if you specify bounds but not enough bounds.
136+
* If you specify *too many* bounds, I think it is reasonable to either:
137+
* warn against using too many bounds
138+
* or enforce them
139+
* Another argument to enforce them: you might put a type alias in your public API, and want those additional bounds for forward-compatibility
140+
141+
Felix:
142+
143+
* I feel similarly but I think you should be allowed to specify extra bounds (and they should be enforced)
144+
* i.e., you might want to have a type alias that accepts a smaller set than the full type could accept
145+
146+
Niko:
147+
148+
* I feel similarly that I think you should be able to write too many bounds and they should be enforced, but that implied bounds is probably more ergonomic
149+
* One hesitation around implied bounds:
150+
* Before the rule for "where you had to write all the bounds" was "in types you do, in fns/impls you don't", but this makes it a bit more subtle
151+
* basically in structs/enums/unions/traits you do :)
152+
* "nominal type declarations" or "definitions" (as opposed to aliases)
153+
154+
### Related case
155+
156+
Functions:
157+
158+
```rust
159+
for<'a> fn(Foo<&'a T>, Foo<&'static T>)
160+
161+
for<'a> where WellFormed(Foo<&'a T>), WellFormed(Foo<&'static T>) fn(
162+
Foo<&'a T>, // <-- accepted today
163+
Foo<&'static T>, // <-- error today
164+
)
165+
```
166+
167+
One compromise might be linting (warning, at least) against types that are certainly going to be errors (like https://github.com/rust-lang/rust/pull/69741).
168+
169+
```
170+
fn foo() where Vec<u8>: Display { }
171+
```
172+
173+
## Immediate step:
174+
175+
Issue warning for cases where we know it must be illegal to use the type alias.
176+
177+
Could be future-compatibility but it's not clear this will ever be a hard error.

0 commit comments

Comments
 (0)