You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/solve/coinduction.md
+56-24Lines changed: 56 additions & 24 deletions
Original file line number
Diff line number
Diff line change
@@ -1,17 +1,20 @@
1
1
# Coinduction
2
2
3
-
The trait solver may use coinduction when proving goals. Coinduction is fairly subtle so we're giving it its own chapter.
3
+
The trait solver may use coinduction when proving goals.
4
+
Coinduction is fairly subtle so we're giving it its own chapter.
4
5
5
6
## Coinduction and induction
6
7
7
-
With induction, we recursively apply proofs until we end up with a finite proof tree. Consider the example of `Vec<Vec<Vec<u32>>>: Debug` which results in the following tree.
8
+
With induction, we recursively apply proofs until we end up with a finite proof tree.
9
+
Consider the example of `Vec<Vec<Vec<u32>>>: Debug` which results in the following tree.
8
10
9
11
-`Vec<Vec<Vec<u32>>>: Debug`
10
12
-`Vec<Vec<u32>>: Debug`
11
13
- `Vec<u32>: Debug`
12
14
- `u32: Debug`
13
15
14
-
This tree is finite. But not all goals we would want to hold have finite proof trees, consider the following example:
16
+
This tree is finite. But not all goals we would want to hold have finite proof trees,
17
+
consider the following example:
15
18
16
19
```rust
17
20
structList<T> {
@@ -20,7 +23,8 @@ struct List<T> {
20
23
}
21
24
```
22
25
23
-
For `List<T>: Send` to hold all its fields have to recursively implement `Send` as well. This would result in the following proof tree:
26
+
For `List<T>: Send` to hold all its fields have to recursively implement `Send` as well.
27
+
This would result in the following proof tree:
24
28
25
29
-`List<T>: Send`
26
30
-`T: Send`
@@ -57,9 +61,9 @@ With cycles we have to be careful with caching. Due to canonicalization of regio
57
61
variables we also have to rerun queries until the provisional result returned when hitting the cycle
58
62
is equal to the final result.
59
63
60
-
TODO: elaborate here. We use the same approach as chalk for coinductive cycles. Note that the treatment
61
-
for inductive cycles currently differs by simply returning `Overflow`. See [the relevant chapters][chalk]
62
-
in the chalk book.
64
+
TODO: elaborate here. We use the same approach as chalk for coinductive cycles.
65
+
Note that the treatment for inductive cycles currently differs by simply returning `Overflow`.
66
+
See [the relevant chapters][chalk]in the chalk book.
@@ -90,20 +94,29 @@ impl<T: Clone> Clone for List<T> {
90
94
}
91
95
```
92
96
93
-
We are using `tail.clone()` in this impl. For this we have to prove `Box<List<T>>: Clone` which requires `List<T>: Clone` but that relies on the currently impl which we are currently checking. By adding that requirement to the `where`-clauses of the impl, which is what we would do with [perfect derive], we move that cycle into the trait solver and [get an error][ex1].
97
+
We are using `tail.clone()` in this impl. For this we have to prove `Box<List<T>>: Clone`
98
+
which requires `List<T>: Clone` but that relies on the currently impl which we are currently
99
+
checking. By adding that requirement to the `where`-clauses of the impl, which is what we would
100
+
do with [perfect derive], we move that cycle into the trait solver and [get an error][ex1].
94
101
95
102
### Recursive data types
96
103
97
-
We also need coinduction to reason about recursive types containing projections, e.g. the following currently fails to compile even though it should be valid.
104
+
We also need coinduction to reason about recursive types containing projections,
105
+
e.g. the following currently fails to compile even though it should be valid.
98
106
```rust
99
107
usestd::borrow::Cow;
100
108
pubstructFoo<'a>(Cow<'a, [Foo<'a>]>);
101
109
```
102
-
This issue has been known since at least 2015, see [#23714](https://github.com/rust-lang/rust/issues/23714) if you want to know more.
110
+
This issue has been known since at least 2015, see
111
+
[#23714](https://github.com/rust-lang/rust/issues/23714) if you want to know more.
103
112
104
113
### Explicitly checked implied bounds
105
114
106
-
When checking an impl, we assume that the types in the impl headers are well-formed. This means that when using instantiating the impl we have to prove that's actually the case. [#100051](https://github.com/rust-lang/rust/issues/100051) shows that this is not the case. To fix this, we have to add `WF` predicates for the types in impl headers. Without coinduction for all traits, this even breaks `core`.
115
+
When checking an impl, we assume that the types in the impl headers are well-formed.
116
+
This means that when using instantiating the impl we have to prove that's actually the case.
117
+
[#100051](https://github.com/rust-lang/rust/issues/100051) shows that this is not the case.
118
+
To fix this, we have to add `WF` predicates for the types in impl headers.
119
+
Without coinduction for all traits, this even breaks `core`.
107
120
108
121
```rust
109
122
traitFromResidual<R> {}
@@ -123,7 +136,7 @@ When checking that the impl of `FromResidual` is well formed we get the followin
123
136
The impl is well formed if `<Ready<T> as Try>::Residual` and `Ready<T>` are well formed.
124
137
-`wf(<Ready<T> as Try>::Residual)` requires
125
138
-`Ready<T>: Try`, which requires because of the super trait
126
-
-`Ready<T>: FromResidual<Ready<T> as Try>::Residual>`, which has an impl which requires **because of implied bounds**
139
+
-`Ready<T>: FromResidual<Ready<T> as Try>::Residual>`, **because of implied bounds on impl**
127
140
-`wf(<Ready<T> as Try>::Residual)`:tada:**cycle**
128
141
129
142
### Issues when extending coinduction to more goals
@@ -133,9 +146,13 @@ The issues here are not relevant for the current solver.
133
146
134
147
#### Implied super trait bounds
135
148
136
-
Our trait system currectly treats super traits, e.g. `trait Trait: SuperTrait`, by 1) requiring that `SuperTrait` has to hold for all types which implement `Trait`, and 2) assuming `SuperTrait` holds if `Trait` holds.
149
+
Our trait system currectly treats super traits, e.g. `trait Trait: SuperTrait`,
150
+
by 1) requiring that `SuperTrait` has to hold for all types which implement `Trait`,
151
+
and 2) assuming `SuperTrait` holds if `Trait` holds.
137
152
138
-
Relying on 2) while proving 1) is unsound. This can only be observed in case of coinductive cycles. Without a cycles, whenever we rely on 2) we must have also proven 1) without relying on 2) for the used impl of `Trait`.
153
+
Relying on 2) while proving 1) is unsound. This can only be observed in case of
154
+
coinductive cycles. Without a cycles, whenever we rely on 2) we must have also
155
+
proven 1) without relying on 2) for the used impl of `Trait`.
139
156
140
157
```rust
141
158
traitTrait:SuperTrait {}
@@ -148,21 +165,25 @@ fn sup<T: SuperTrait>() {}
148
165
fnrequires_trait<T:Trait>() { sup::<T>() }
149
166
fngeneric<T>() { requires_trait::<T>() }
150
167
```
151
-
This is not really fundamental to coinduction but rather an existing property which is made unsound because of it.
168
+
This is not really fundamental to coinduction but rather an existing property
169
+
which is made unsound because of it.
152
170
153
171
##### Possible solutions
154
172
155
173
The easiest way to solve this would be to completely remove 2) and always elaborate
156
174
`T: Trait` to `T: Trait` and `T: SuperTrait` outside of the trait solver.
157
-
This would allow us to also remove 1), but as we still have to prove ordinary`where`-bounds on traits,
158
-
that's just additional work.
175
+
This would allow us to also remove 1), but as we still have to prove ordinary
176
+
`where`-bounds on traits, that's just additional work.
159
177
160
-
While one could imagine ways to disable cyclic uses of 2) when checking 1), at least the ideas of myself - @lcnr -
161
-
are all far to complex to be reasonable.
178
+
While one could imagine ways to disable cyclic uses of 2) when checking 1),
179
+
at least the ideas of myself - @lcnr - are all far to complex to be reasonable.
162
180
163
181
#### `normalizes_to` goals and progress
164
182
165
-
A `normalizes_to` goal represents the requirement that `<T as Trait>::Assoc` normalizes to some `U`. This is achieved by defacto first normalizing `<T as Trait>::Assoc` and then equating the resulting type with `U`. It should be a mapping as each projection should normalize to exactly one type. By simply allowing infinite proof trees, we would get the following behavior:
183
+
A `normalizes_to` goal represents the requirement that `<T as Trait>::Assoc` normalizes
184
+
to some `U`. This is achieved by defacto first normalizing `<T as Trait>::Assoc` and then
185
+
equating the resulting type with `U`. It should be a mapping as each projection should normalize
186
+
to exactly one type. By simply allowing infinite proof trees, we would get the following behavior:
166
187
167
188
```rust
168
189
traitTrait {
@@ -174,19 +195,30 @@ impl Trait for () {
174
195
}
175
196
```
176
197
177
-
If we now compute `normalizes_to(<() as Trait>::Assoc, Vec<u32>)`, we would resolve the impl and get the associated type `<() as Trait>::Assoc`. We then equate that with the expected type, causing us to check `normalizes_to(<() as Trait>::Assoc, Vec<u32>)` again. This just goes on forever, resulting in an infinite proof tree.
198
+
If we now compute `normalizes_to(<() as Trait>::Assoc, Vec<u32>)`, we would resolve the impl
199
+
and get the associated type `<() as Trait>::Assoc`. We then equate that with the expected type,
200
+
causing us to check `normalizes_to(<() as Trait>::Assoc, Vec<u32>)` again.
201
+
This just goes on forever, resulting in an infinite proof tree.
178
202
179
203
This means that `<() as Trait>::Assoc` would be equal to any other type which is unsound.
180
204
181
205
##### How to solve this
182
206
183
207
**WARNING: THIS IS SUBTLE AND MIGHT BE WRONG**
184
208
185
-
Unlike trait goals, `normalizes_to` has to be *productive*[^1]. A `normalizes_to` goal is productive once the projection normalizes to a rigid type constructor, so `<() as Trait>::Assoc` normalizing to `Vec<<() as Trait>::Assoc>` would be productive.
209
+
Unlike trait goals, `normalizes_to` has to be *productive*[^1]. A `normalizes_to` goal
210
+
is productive once the projection normalizes to a rigid type constructor,
211
+
so `<() as Trait>::Assoc` normalizing to `Vec<<() as Trait>::Assoc>` would be productive.
186
212
187
-
A `normalizes_to` goal has two kinds of nested goals. Nested requirements needed to actually normalize the projection, and the equality between the normalized projection and the expected type. Only the equality has to be productive. A branch in the proof tree is productive if it is either finite, or contains at least one `normalizes_to` where the alias is resolved to a rigid type constructor.
213
+
A `normalizes_to` goal has two kinds of nested goals. Nested requirements needed to actually
214
+
normalize the projection, and the equality between the normalized projection and the
215
+
expected type. Only the equality has to be productive. A branch in the proof tree is productive
216
+
if it is either finite, or contains at least one `normalizes_to` where the alias is resolved
217
+
to a rigid type constructor.
188
218
189
-
Alternatively, we could simply always treat the equate branch of `normalizes_to` as inductive. Any cycles should result in infinite types, which aren't supported anyways and would only result in overflow when deeply normalizing for codegen.
219
+
Alternatively, we could simply always treat the equate branch of `normalizes_to` as inductive.
220
+
Any cycles should result in infinite types, which aren't supported anyways and would only
221
+
result in overflow when deeply normalizing for codegen.
190
222
191
223
experimentation and examples: https://hackmd.io/-8p0AHnzSq2VAE6HE_wX-w?view
0 commit comments