Skip to content

Commit 7b548db

Browse files
committed
Go back to reducing match types sequentially
However, need non-overlapping patterns in order to discard a pattern.
1 parent 5a8ac63 commit 7b548db

File tree

3 files changed

+45
-40
lines changed

3 files changed

+45
-40
lines changed

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

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3595,10 +3595,38 @@ object Types {
35953595

35963596
override def tryNormalize(implicit ctx: Context): Type = reduced.normalized
35973597

3598+
/** Switch to choose parallel or sequential reduction */
3599+
private final val reduceInParallel = false
3600+
3601+
final def cantPossiblyMatch(cas: Type)(implicit ctx: Context) =
3602+
true // should be refined if we allow overlapping cases
3603+
35983604
def reduced(implicit ctx: Context): Type = {
35993605
val trackingCtx = ctx.fresh.setTypeComparerFn(new TrackingTypeComparer(_))
36003606
val cmp = trackingCtx.typeComparer.asInstanceOf[TrackingTypeComparer]
36013607

3608+
def reduceSequential(cases: List[Type])(implicit ctx: Context): Type = cases match {
3609+
case Nil => NoType
3610+
case cas :: cases1 =>
3611+
val r = cmp.matchCase(scrutinee, cas, instantiate = true)
3612+
if (r.exists) r
3613+
else if (cantPossiblyMatch(cas)) reduceSequential(cases1)
3614+
else NoType
3615+
}
3616+
3617+
def reduceParallel(implicit ctx: Context) = {
3618+
val applicableBranches = cases
3619+
.map(cmp.matchCase(scrutinee, _, instantiate = true)(trackingCtx))
3620+
.filter(_.exists)
3621+
applicableBranches match {
3622+
case Nil => NoType
3623+
case applicableBranch :: Nil => applicableBranch
3624+
case _ =>
3625+
record(i"MatchType.multi-branch")
3626+
ctx.typeComparer.glb(applicableBranches)
3627+
}
3628+
}
3629+
36023630
def isRelevant(tp: Type) = tp match {
36033631
case tp: TypeParamRef => ctx.typerState.constraint.entry(tp).exists
36043632
case tp: TypeRef => ctx.gadt.bounds.contains(tp.symbol)
@@ -3631,18 +3659,13 @@ object Types {
36313659
if (myReduced != null) record("MatchType.reduce cache miss")
36323660
myReduced =
36333661
trace(i"reduce match type $this", typr, show = true) {
3634-
if (defn.isBottomType(scrutinee)) defn.NothingType
3635-
else {
3636-
val applicableBranches = cases
3637-
.map(cmp.matchCase(scrutinee, _, instantiate = true)(trackingCtx))
3638-
.filter(_.exists)
3639-
applicableBranches match {
3640-
case Nil => NoType
3641-
case applicableBranch :: Nil => applicableBranch
3642-
case _ =>
3643-
record(i"MatchType.multi-branch")
3644-
ctx.typeComparer.glb(applicableBranches)
3645-
}
3662+
try
3663+
if (defn.isBottomType(scrutinee)) defn.NothingType
3664+
else if (reduceInParallel) reduceParallel(trackingCtx)
3665+
else reduceSequential(cases)(trackingCtx)
3666+
catch {
3667+
case ex: Throwable =>
3668+
handleRecursive("reduce type ", i"$scrutinee match ...", ex)
36463669
}
36473670
}
36483671
updateReductionContext()

docs/docs/reference/match-types.md

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,12 @@ Using `can-reduce`, we can now define match type reduction proper in the `reduce
8484
```
8585
Match(S, C1, ..., Cn) reduces-to T
8686
```
87-
if `Ci_1, ..., Ci_k` is a maximal non-empty subset of `C1, ..., Cn` such that for each `i_j`:
87+
if
8888
```
89-
Match(S, C1, ..., Cn) can-reduce i_j, Ti_j
89+
Match(S, C1, ..., Cn) can-reduce i, T
9090
```
91-
and
92-
```
93-
T = Ti_1 & ... & Ti_k
94-
```
95-
In other words, a match reduces to the intersection of all right hand sides it can reduce to. This "parallel" notion of reduction was picked for its nice algebraic properties, even though it does not correspond directly to the operational semantics of pattern matching on terms, where the first matching case is chosen.
91+
and, for `j` in `1..i-1`: `C_j` is disjoint from `C_i`, or else `S` cannot possibly match `C_j`.
92+
See the section on overlapping patterns for an elaboration of "disjoint" and "cannot possibly match".
9693

9794
## Subtyping Rules for Match Types
9895

@@ -159,26 +156,11 @@ the fact that `s.type` must conform to the pattern's type and derive a GADT cons
159156
this would be the constraint `x.type <: A`. The new aspect here is that we need GADT constraints over singleton types where
160157
before we just had constraints over type parameters.
161158

162-
Assuming this extension, we can then try to typecheck as usual. E.g. to typecheck the first case `case _: A => 1` of the definition of `m` above, GADT matching will produce the constraint `x.type <: A`. Therefore, `M[x.type]` reduces to the singleton type `1`. The right hand side `1` of the case conforms to this type, so the case typechecks. Typechecking the second case proceeds similarly.
163-
164-
However, it turns out that these rules are not enough for type soundness. To see this, assume that `A` and `B` are traits that are both extended by a common class `C`. In this case, and assuming `c: C`, `M[c.type]` reduces to `1 & 2`, but `m(c)` reduces to `1`. So the type of the application `m(c)` does not match the reduced result type of `m`, which means soundness is violated.
165-
166-
To plug the soundness hole, we have to tighten the typing rules for match expressions. In the example above we need to also consider the case where the scrutinee `x` conforms to `A` and `B`. In this case, the match expression still returns `1` but the match type `M[x.type]` reduces to `1 & 2`, which means there should be a type error. However, this second check can be omitted if `A` and `B` are types that don't overlap. We can omit the check because in that case there is no scrutinee value `x` that could reduce to `1`, so no discrepancy can arise at runtime.
167-
168-
More generally, we proceeed as follows:
169-
170-
When typechecking the `i`th case of a match expression
171-
```
172-
x match { case P_1 => t_1 ... case P_n => t_n
173-
```
174-
where `t_i` has type `T_i` against an expected match type `R`:
175-
176-
1. Determine all maximal sequences of
177-
patterns `P_j_1, ..., P_j_m` that follow `P_i` in the match expression and that do overlap with `P_i`. That is, `P_i, P_j_1, ..., P_j_m` all match at least one common value.
178-
179-
2. For each such sequence, verify that `T_i <: R` under the GADT constraint arising from matching the scrutinee `x` against all of the patterns `P_i, P_j_1, ..., P_j_m`.
159+
Assuming this extension, we can then try to typecheck as usual. E.g. to typecheck the first case `case _: A => 1` of the definition of `m` above, GADT matching will produce the constraint `x.type <: A`. Therefore, `M[x.type]` reduces to the singleton type `1`. The right hand side `1` of the case conforms to this type, so the case typechecks.
180160

181-
In the example above, `A` and `B` would be overlapping because they have the common subclass `C`. Hence, we have to check that the right-hand side `1` is a subtype of `M[x.type]` under the assumptions that `x.type <: A` and `x.type <: B`. Under these assumptions `M[x.type]` reduces to `1 & 2`, which gives a type error.
161+
Typechecking the second case hits a snag, though. In general, the assumption `x.type <: B` is not enough to prove that
162+
`M[x.type]` reduces to `2`. However we can reduce `M[x.type]` to `2` if the types `A` and `B` do not overlap.
163+
So correspondence of match terms to match types is feasible only in the case of non-overlapping patterns.
182164

183165
For simplicity, we have disregarded the `null` value in this discussion. `null` does not cause a fundamental problem but complicates things somewhat because some forms of patterns do not match `null`.
184166

tests/neg-custom-args/matchtype-loop.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ object Test {
1111
val x: Int = g[Int] // error: found: L[Int], required: Int
1212

1313
def aa: LL[Boolean] = ???
14-
def bb: LL[Int] = ??? // error: recursion limit exceeded with subtype LazyRef(Test.LL[Int]) <:< Int
14+
def bb: LL[Int] = ??? // error: recursion limit exceeded with reduce type LazyRef(Test.LL[Int]) match ...
1515
def gg[X]: LL[X] = ???
16-
val xx: Int = gg[Int] // error: recursion limit exceeded with subtype LazyRef(Test.LL[Int]) <:< Int
16+
val xx: Int = gg[Int] // error: recursion limit exceeded with reduce type LazyRef(Test.LL[Int]) match ...
1717
}

0 commit comments

Comments
 (0)