Skip to content

Commit decfade

Browse files
oderskyKordyjan
authored andcommitted
Refine Matchtype checking
Take up scala#13780 again, but refine it so that abstract types are allowed in match type reduction as long as they uniquely instantiate type parameters of the type pattern. Fixes scala#11982
1 parent 16f5c25 commit decfade

File tree

12 files changed

+288
-33
lines changed

12 files changed

+288
-33
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package core
44

55
import Types._, Contexts._, Symbols._, Decorators._
66
import util.Property
7+
import Names.Name
78

89
/** A utility module to produce match type reduction traces in error messages.
910
*/
@@ -13,6 +14,7 @@ object MatchTypeTrace:
1314
case TryReduce(scrut: Type)
1415
case NoMatches(scrut: Type, cases: List[Type])
1516
case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])
17+
case NoInstance(scrut: Type, stuckCase: Type, pname: Name, bounds: TypeBounds)
1618
case EmptyScrutinee(scrut: Type)
1719
import TraceEntry._
1820

@@ -62,6 +64,9 @@ object MatchTypeTrace:
6264
def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) =
6365
matchTypeFail(Stuck(scrut, stuckCase, otherCases))
6466

67+
def noInstance(scrut: Type, stuckCase: Type, pname: Name, bounds: TypeBounds)(using Context) =
68+
matchTypeFail(NoInstance(scrut, stuckCase, pname, bounds))
69+
6570
/** Record a failure that scrutinee `scrut` is provably empty.
6671
* Only the first failure is recorded.
6772
*/
@@ -114,6 +119,10 @@ object MatchTypeTrace:
114119
| Therefore, reduction cannot advance to the remaining case$s
115120
|
116121
| ${casesText(otherCases)}"""
122+
case NoInstance(scrut, stuckCase, pname, bounds) =>
123+
i""" failed since selector $scrut
124+
| does not uniquely determine parameter $pname in ${caseText(stuckCase)}
125+
| The computed bounds for the parameter are: $bounds."""
117126

118127
def noMatchesText(scrut: Type, cases: List[Type])(using Context): String =
119128
i"""failed since selector $scrut

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

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
6060
/** Indicates whether the subtype check used GADT bounds */
6161
private var GADTused: Boolean = false
6262

63+
protected var canWidenAbstract: Boolean = true
64+
6365
private var myInstance: TypeComparer = this
6466
def currentInstance: TypeComparer = myInstance
6567

@@ -757,9 +759,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
757759

758760
def tryBaseType(cls2: Symbol) = {
759761
val base = nonExprBaseType(tp1, cls2)
760-
if (base.exists && (base `ne` tp1))
761-
isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow) ||
762-
base.isInstanceOf[OrType] && fourthTry
762+
if base.exists && (base ne tp1)
763+
&& (!caseLambda.exists || canWidenAbstract || tp1.widen.underlyingClassRef(refinementOK = true).exists)
764+
then
765+
isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow)
766+
|| base.isInstanceOf[OrType] && fourthTry
763767
// if base is a disjunction, this might have come from a tp1 type that
764768
// expands to a match type. In this case, we should try to reduce the type
765769
// and compare the redux. This is done in fourthTry
@@ -776,7 +780,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
776780
|| narrowGADTBounds(tp1, tp2, approx, isUpper = true))
777781
&& (tp2.isAny || GADTusage(tp1.symbol))
778782

779-
isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
783+
(!caseLambda.exists || canWidenAbstract) && isSubType(hi1, tp2, approx.addLow)
784+
|| compareGADT
785+
|| tryLiftedToThis1
780786
case _ =>
781787
// `Mode.RelaxedOverriding` is only enabled when checking Java overriding
782788
// in explicit nulls, and `Null` becomes a bottom type, which allows
@@ -2849,7 +2855,16 @@ object TypeComparer {
28492855
comparing(_.tracked(op))
28502856
}
28512857

2858+
object TrackingTypeComparer:
2859+
enum MatchResult:
2860+
case Reduced(tp: Type)
2861+
case Disjoint
2862+
case Stuck
2863+
case NoInstance(param: Name, bounds: TypeBounds)
2864+
28522865
class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
2866+
import TrackingTypeComparer.*
2867+
28532868
init(initctx)
28542869

28552870
override def trackingTypeComparer = this
@@ -2887,15 +2902,25 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
28872902
}
28882903

28892904
def matchCases(scrut: Type, cases: List[Type])(using Context): Type = {
2890-
def paramInstances = new TypeAccumulator[Array[Type]] {
2891-
def apply(inst: Array[Type], t: Type) = t match {
2892-
case t @ TypeParamRef(b, n) if b `eq` caseLambda =>
2893-
inst(n) = approximation(t, fromBelow = variance >= 0).simplified
2894-
inst
2905+
2906+
def paramInstances(canApprox: Boolean) = new TypeAccumulator[Array[Type]]:
2907+
def apply(insts: Array[Type], t: Type) = t match
2908+
case param @ TypeParamRef(b, n) if b eq caseLambda =>
2909+
insts(n) = {
2910+
if canApprox then
2911+
approximation(param, fromBelow = variance >= 0)
2912+
else constraint.entry(param) match
2913+
case entry: TypeBounds =>
2914+
val lo = fullLowerBound(param)
2915+
val hi = fullUpperBound(param)
2916+
if isSubType(hi, lo) then lo else TypeBounds(lo, hi)
2917+
case inst =>
2918+
assert(inst.exists, i"param = $param\nconstraint = $constraint")
2919+
inst
2920+
}.simplified
2921+
insts
28952922
case _ =>
2896-
foldOver(inst, t)
2897-
}
2898-
}
2923+
foldOver(insts, t)
28992924

29002925
def instantiateParams(inst: Array[Type]) = new TypeMap {
29012926
def apply(t: Type) = t match {
@@ -2911,7 +2936,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
29112936
* None if the match fails and we should consider the following cases
29122937
* because scrutinee and pattern do not overlap
29132938
*/
2914-
def matchCase(cas: Type): Option[Type] = trace(i"match case $cas vs $scrut", matchTypes) {
2939+
def matchCase(cas: Type): MatchResult = trace(i"match case $cas vs $scrut", matchTypes) {
29152940
val cas1 = cas match {
29162941
case cas: HKTypeLambda =>
29172942
caseLambda = constrained(cas)
@@ -2922,34 +2947,48 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
29222947

29232948
val defn.MatchCase(pat, body) = cas1: @unchecked
29242949

2925-
if (isSubType(scrut, pat))
2926-
// `scrut` is a subtype of `pat`: *It's a Match!*
2927-
Some {
2928-
caseLambda match {
2929-
case caseLambda: HKTypeLambda =>
2930-
val instances = paramInstances(new Array(caseLambda.paramNames.length), pat)
2931-
instantiateParams(instances)(body).simplified
2932-
case _ =>
2933-
body
2934-
}
2935-
}
2950+
def matches(canWidenAbstract: Boolean): Boolean =
2951+
val saved = this.canWidenAbstract
2952+
this.canWidenAbstract = canWidenAbstract
2953+
try necessarySubType(scrut, pat)
2954+
finally this.canWidenAbstract = saved
2955+
2956+
def redux(canApprox: Boolean): MatchResult =
2957+
caseLambda match
2958+
case caseLambda: HKTypeLambda =>
2959+
val instances = paramInstances(canApprox)(new Array(caseLambda.paramNames.length), pat)
2960+
instances.indices.find(instances(_).isInstanceOf[TypeBounds]) match
2961+
case Some(i) if !canApprox =>
2962+
MatchResult.NoInstance(caseLambda.paramNames(i), instances(i).bounds)
2963+
case _ =>
2964+
MatchResult.Reduced(instantiateParams(instances)(body).simplified)
2965+
case _ =>
2966+
MatchResult.Reduced(body)
2967+
2968+
if caseLambda.exists && matches(canWidenAbstract = false) then
2969+
redux(canApprox = true)
2970+
else if matches(canWidenAbstract = true) then
2971+
redux(canApprox = false)
29362972
else if (provablyDisjoint(scrut, pat))
29372973
// We found a proof that `scrut` and `pat` are incompatible.
29382974
// The search continues.
2939-
None
2975+
MatchResult.Disjoint
29402976
else
2941-
Some(NoType)
2977+
MatchResult.Stuck
29422978
}
29432979

29442980
def recur(remaining: List[Type]): Type = remaining match
29452981
case cas :: remaining1 =>
29462982
matchCase(cas) match
2947-
case None =>
2983+
case MatchResult.Disjoint =>
29482984
recur(remaining1)
2949-
case Some(NoType) =>
2985+
case MatchResult.Stuck =>
29502986
MatchTypeTrace.stuck(scrut, cas, remaining1)
29512987
NoType
2952-
case Some(tp) =>
2988+
case MatchResult.NoInstance(pname, bounds) =>
2989+
MatchTypeTrace.noInstance(scrut, cas, pname, bounds)
2990+
NoType
2991+
case MatchResult.Reduced(tp) =>
29532992
tp
29542993
case Nil =>
29552994
val casesText = MatchTypeTrace.noMatchesText(scrut, cases)

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ class CompilationTests {
200200
@Test def runAll: Unit = {
201201
implicit val testGroup: TestGroup = TestGroup("runAll")
202202
aggregateTests(
203+
compileFile("tests/run-custom-args/typeclass-derivation1.scala", defaultOptions.without(yCheckOptions*)),
203204
compileFile("tests/run-custom-args/tuple-cons.scala", allowDeepSubtypes),
204205
compileFile("tests/run-custom-args/i5256.scala", allowDeepSubtypes),
205206
compileFile("tests/run-custom-args/no-useless-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),

tests/neg/6570-1.check

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- [E007] Type Mismatch Error: tests/neg/6570-1.scala:23:27 ------------------------------------------------------------
2+
23 | def thing = new Trait1 {} // error
3+
| ^
4+
| Found: Object with Trait1 {...}
5+
| Required: N[Box[Int & String]]
6+
|
7+
| Note: a match type could not be fully reduced:
8+
|
9+
| trying to reduce N[Box[Int & String]]
10+
| failed since selector Box[Int & String]
11+
| is uninhabited (there are no values of that type).
12+
|
13+
| longer explanation available when compiling with `-explain`
14+
-- [E007] Type Mismatch Error: tests/neg/6570-1.scala:36:54 ------------------------------------------------------------
15+
36 | def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing // error
16+
| ^^^^^^^
17+
| Found: M[T]
18+
| Required: Trait2
19+
|
20+
| where: T is a type in method foo with bounds <: Cov[Box[Int]]
21+
|
22+
|
23+
| Note: a match type could not be fully reduced:
24+
|
25+
| trying to reduce M[T]
26+
| failed since selector T
27+
| does not uniquely determine parameter x in case Cov[x] => N[x]
28+
| The computed bounds for the parameter are: >: Box[Int].
29+
|
30+
| longer explanation available when compiling with `-explain`

tests/neg/6570-1.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Asploder extends Root[Cov[Box[Int & String]]] {
3333
}
3434

3535
object Main {
36-
def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing
36+
def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing // error
3737
def explode = foo(new Asploder)
3838

3939
def main(args: Array[String]): Unit =

tests/neg/6570.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ object UpperBoundParametricVariant {
2121
trait Child[A <: Cov[Int]] extends Root[A]
2222

2323
// we reduce `M[T]` to `Trait2`, even though we cannot be certain of that
24-
def foo[T <: Cov[Int]](c: Child[T]): Trait2 = c.thing
24+
def foo[T <: Cov[Int]](c: Child[T]): Trait2 = c.thing // error
2525

2626
class Asploder extends Child[Cov[String & Int]] {
2727
def thing = new Trait1 {} // error
@@ -42,7 +42,7 @@ object InheritanceVariant {
4242

4343
trait Child extends Root { type B <: { type A <: Int } }
4444

45-
def foo(c: Child): Trait2 = c.thing
45+
def foo(c: Child): Trait2 = c.thing // error
4646

4747
class Asploder extends Child {
4848
type B = { type A = String & Int }
@@ -98,7 +98,7 @@ object UpperBoundVariant {
9898

9999
trait Child extends Root { type A <: Cov[Int] }
100100

101-
def foo(c: Child): Trait2 = c.thing
101+
def foo(c: Child): Trait2 = c.thing // error
102102

103103
class Asploder extends Child {
104104
type A = Cov[String & Int]

tests/neg/i11982.check

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
-- [E057] Type Mismatch Error: tests/neg/i11982.scala:9:34 -------------------------------------------------------------
2+
9 | b: ValueOf[Tuple.Head[Tuple.Tail[X]]] // error
3+
| ^
4+
| Type argument Tuple.Tail[X] does not conform to upper bound NonEmptyTuple
5+
|
6+
| Note: a match type could not be fully reduced:
7+
|
8+
| trying to reduce Tuple.Tail[X]
9+
| failed since selector X
10+
| does not uniquely determine parameter xs in case _ *: xs => xs
11+
| The computed bounds for the parameter are: >: Any *: EmptyTuple.type <: Tuple.
12+
|
13+
| longer explanation available when compiling with `-explain`
14+
-- [E057] Type Mismatch Error: tests/neg/i11982.scala:10:38 ------------------------------------------------------------
15+
10 | ): Tuple2[Tuple.Head[X], Tuple.Head[Tuple.Tail[X]]] = // error
16+
| ^
17+
| Type argument Tuple.Tail[X] does not conform to upper bound NonEmptyTuple
18+
|
19+
| Note: a match type could not be fully reduced:
20+
|
21+
| trying to reduce Tuple.Tail[X]
22+
| failed since selector X
23+
| does not uniquely determine parameter xs in case _ *: xs => xs
24+
| The computed bounds for the parameter are: >: Any *: EmptyTuple.type <: Tuple.
25+
|
26+
| longer explanation available when compiling with `-explain`
27+
-- [E057] Type Mismatch Error: tests/neg/i11982.scala:12:25 ------------------------------------------------------------
28+
12 | type BB = Tuple.Head[Tuple.Tail[X]] // error
29+
| ^
30+
| Type argument Tuple.Tail[X] does not conform to upper bound NonEmptyTuple
31+
|
32+
| Note: a match type could not be fully reduced:
33+
|
34+
| trying to reduce Tuple.Tail[X]
35+
| failed since selector X
36+
| does not uniquely determine parameter xs in case _ *: xs => xs
37+
| The computed bounds for the parameter are: >: Any *: EmptyTuple.type <: Tuple.
38+
|
39+
| longer explanation available when compiling with `-explain`

tests/neg/i11982.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package tuplefun
2+
object Unpair {
3+
4+
def pair[A, B](using a: ValueOf[A], b: ValueOf[B]): Tuple2[A, B] =
5+
(a.value, b.value)
6+
7+
def unpair[X <: Tuple2[?, ?]](
8+
using a: ValueOf[Tuple.Head[X]],
9+
b: ValueOf[Tuple.Head[Tuple.Tail[X]]] // error
10+
): Tuple2[Tuple.Head[X], Tuple.Head[Tuple.Tail[X]]] = // error
11+
type AA = Tuple.Head[X]
12+
type BB = Tuple.Head[Tuple.Tail[X]] // error
13+
pair[AA, BB](using a, b)
14+
}
15+
16+
object UnpairApp {
17+
import Unpair._
18+
19+
type Tshape = ("msg", 42)
20+
21+
// the following won't compile when in the same file as Unpair
22+
val p1: ("msg", 42) = unpair[Tshape]
23+
24+
@main def pairHello: Unit =
25+
assert(p1 == ("msg", 42))
26+
println(p1)
27+
}

tests/neg/i13780.check

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i13780.scala:18:31 ------------------------------------------------------------
2+
18 | def int[X <: Y]: Int = unpair[X] // error
3+
| ^^^^^^^^^
4+
| Found: Head[X]
5+
| Required: Int
6+
|
7+
| where: X is a type in method int with bounds <: B.this.Y
8+
|
9+
|
10+
| Note: a match type could not be fully reduced:
11+
|
12+
| trying to reduce Head[X]
13+
| failed since selector X
14+
| does not uniquely determine parameter a in case (a, b) => a
15+
| The computed bounds for the parameter are: >: Int.
16+
|
17+
| longer explanation available when compiling with `-explain`
18+
-- [E007] Type Mismatch Error: tests/neg/i13780.scala:23:37 ------------------------------------------------------------
19+
23 | def string[X <: Y]: String = unpair[X] // error
20+
| ^^^^^^^^^
21+
| Found: Head[X]
22+
| Required: String
23+
|
24+
| where: X is a type in method string with bounds <: C.this.Y
25+
|
26+
|
27+
| Note: a match type could not be fully reduced:
28+
|
29+
| trying to reduce Head[X]
30+
| failed since selector X
31+
| does not uniquely determine parameter a in case (a, b) => a
32+
| The computed bounds for the parameter are: >: String.
33+
|
34+
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)