Skip to content

Commit d64717d

Browse files
committed
Fix matchtype termination
- Don't reduce eagerly when forming matchtypes, as this could generate illegal cycles. - Add section to match-types.md how termination is handled.
1 parent 0ca095f commit d64717d

File tree

3 files changed

+72
-3
lines changed

3 files changed

+72
-3
lines changed

compiler/src/dotty/tools/dotc/core/Types.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -3636,8 +3636,10 @@ object Types {
36363636

36373637
object MatchType {
36383638
def apply(bound: Type, scrutinee: Type, cases: List[Type])(implicit ctx: Context) = {
3639-
val mt = unique(new CachedMatchType(bound, scrutinee, cases))
3640-
mt.reduced.orElse(mt)
3639+
unique(new CachedMatchType(bound, scrutinee, cases))
3640+
// TODO: maybe we should try to reduce match types immediately, but this risks creating illegal
3641+
// cycles. So we can do this only if we can prove that the redux is in some sense simpler than
3642+
// the original type.
36413643
}
36423644
}
36433645

docs/docs/reference/match-types.md

+51-1
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,63 @@ A complete defininition of when two patterns or types overlap still needs to be
183183
The last rule in particular is needed to detect non-overlaps for cases where the scrutinee and the patterns are tuples. I.e. `(Int, String)` does not overlap `(Int, Int)` since
184184
`String` does not overlap `Int`.
185185

186+
## Handling Termination
187+
188+
Match type definitions can be recursive, which raises the question whether and how to check
189+
that reduction terminates. This is currently an open question. We should investigate whether
190+
there are workable ways to enforce that recursion is primitive.
191+
192+
Note that, since reduction is linked to subtyping, we already have a cycle dectection mechanism in place.
193+
So the following will already give a reasonable error message:
194+
```scala
195+
type L[X] = X match {
196+
case Int => L[X]
197+
}
198+
def g[X]: L[X] = ???
199+
val x: Int = g[Int]
200+
```
201+
202+
```
203+
|
204+
| ^^^^^^
205+
| found: Test.L[Int]
206+
| required: Int
207+
```
208+
209+
The subtype cycle test can be circumvented by producing larger types in each recursive invocation, as in the following definitions:
210+
```scala
211+
type LL[X] = X match {
212+
case Int => LL[LL[X]]
213+
}
214+
def gg[X]: LL[X] = ???
215+
```
216+
In this case subtyping enters into an infinite recursion. This is not as bad as it looks, however, because
217+
`dotc` turns selected stack overflows into type errors. If there is a stackoverflow during subtyping,
218+
the exception will be caught and turned into a compile-time error that indicates
219+
a trace of the subtype tests that caused the overflow without showing a full stacktrace.
220+
Concretely:
221+
```
222+
| val xx: Int = gg[Int]
223+
| ^
224+
|Recursion limit exceeded.
225+
|Maybe there is an illegal cyclic reference?
226+
|If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
227+
|A recurring operation is (inner to outer):
228+
|
229+
| subtype Test.LL[Int] <:< Int
230+
| subtype Test.LL[Int] <:< Int
231+
| ...
232+
| subtype Test.LL[Int] <:< Int
233+
```
234+
(The actual error message shows some additional lines in the stacktrace).
235+
186236
## Related Work
187237

188238
Match types have similarities with [closed type families](https://wiki.haskell.org/GHC/Type_families) in Haskell. Some differences are:
189239

190240
- Subtyping instead of type equalities.
191241
- Match type reduction does not tighten the underlying constraint, whereas type family reduction does unify. This difference in approach mirrors the difference between local type inference in Scala and global type inference in Haskell.
192-
- No a-priory requirement that cases are non-overlapping. Uses parallel reduction
242+
- No a-priori requirement that cases are non-overlapping. Uses parallel reduction
193243
instead of always chosing a unique branch.
194244

195245
Match types are also similar to Typescript's [conditional types](https://github.com/Microsoft/TypeScript/pull/21316). The main differences here are:

tests/neg/matchtype-loop.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
object Test {
2+
type L[X] = X match {
3+
case Int => L[X]
4+
}
5+
type LL[X] = X match {
6+
case Int => LL[LL[X]]
7+
}
8+
def a: L[Boolean] = ???
9+
def b: L[Int] = ???
10+
def g[X]: L[X] = ???
11+
def g[X]: L[X] = ??? // error: found: L[Int], required: Int
12+
13+
def aa: LL[Boolean] = ???
14+
def bb: LL[Int] = ??? // error: recursion limit exceeded with subtype LazyRef(Test.LL[Int]) <:< Int
15+
def gg[X]: LL[X] = ???
16+
val xx: Int = gg[Int] // error: recursion limit exceeded with subtype LazyRef(Test.LL[Int]) <:< Int
17+
}

0 commit comments

Comments
 (0)