Skip to content

Commit 194d85d

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 194d85d

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,62 @@ 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+
```
200+
201+
```
202+
| val x: Int = g[Int]
203+
| ^^^^^^
204+
| found: Test.L[Int]
205+
| required: Int
206+
```
207+
208+
The subtype cycle test can be circumvented by producing larger types in each recursive invocation, as in the following definitions:
209+
```scala
210+
type LL[X] = X match {
211+
case Int => LL[LL[X]]
212+
}
213+
def gg[X]: LL[X] = ???
214+
```
215+
In this case subtyping enters into an infinite recursion. This is not as bad as it looks, however, because
216+
`dotc` turns selected stack overflows into type errors. If there is a stackoverflow during subtyping,
217+
the exception will be caught and turned into a compile-time error that indicates
218+
a trace of the subtype tests that caused the overflow without showing a full stacktrace.
219+
Concretely:
220+
```
221+
| val xx: Int = gg[Int]
222+
| ^
223+
|Recursion limit exceeded.
224+
|Maybe there is an illegal cyclic reference?
225+
|If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
226+
|A recurring operation is (inner to outer):
227+
|
228+
| subtype Test.LL[Int] <:< Int
229+
| subtype Test.LL[Int] <:< Int
230+
| ...
231+
| subtype Test.LL[Int] <:< Int
232+
```
233+
(The actual error message shows some additional lines in the stacktrace).
234+
186235
## Related Work
187236

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

190239
- Subtyping instead of type equalities.
191240
- 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
241+
- No a-priori requirement that cases are non-overlapping. Uses parallel reduction
193242
instead of always chosing a unique branch.
194243

195244
Match types are also similar to Typescript's [conditional types](https://github.com/Microsoft/TypeScript/pull/21316). The main differences here are:
@@ -199,6 +248,6 @@ Match types are also similar to Typescript's [conditional types](https://github.
199248
- Match types can bind variables in type patterns.
200249
- Match types support direct recursion.
201250

202-
Conditional types on Typescript distribute through union types. We should evaluate whether match types should support this as well.
251+
Conditional types in Typescript distribute through union types. We should evaluate whether match types should support this as well.
203252

204253

tests/neg/matchtype-loop.scala

Lines changed: 17 additions & 0 deletions
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)