Skip to content

Commit 4cb967f

Browse files
authored
Merge pull request #15423 from dotty-staging/fix-11982
Refine Matchtype checking
2 parents 1f813bc + e9c4539 commit 4cb967f

16 files changed

+356
-44
lines changed

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

+13-1
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, fails: List[(Name, 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, fails: List[(Name, TypeBounds)])(using Context) =
68+
matchTypeFail(NoInstance(scrut, stuckCase, fails))
69+
6570
/** Record a failure that scrutinee `scrut` is provably empty.
6671
* Only the first failure is recorded.
6772
*/
@@ -82,7 +87,7 @@ object MatchTypeTrace:
8287
case _ =>
8388
op
8489

85-
private def caseText(tp: Type)(using Context): String = tp match
90+
def caseText(tp: Type)(using Context): String = tp match
8691
case tp: HKTypeLambda => caseText(tp.resultType)
8792
case defn.MatchCase(any, body) if any eq defn.AnyType => i"case _ => $body"
8893
case defn.MatchCase(pat, body) => i"case $pat => $body"
@@ -114,6 +119,13 @@ object MatchTypeTrace:
114119
| Therefore, reduction cannot advance to the remaining case$s
115120
|
116121
| ${casesText(otherCases)}"""
122+
case NoInstance(scrut, stuckCase, fails) =>
123+
def params = if fails.length == 1 then "parameter" else "parameters"
124+
i""" failed since selector $scrut
125+
| does not uniquely determine $params ${fails.map(_._1)}%, % in
126+
| ${caseText(stuckCase)}
127+
| The computed bounds for the $params are:
128+
| ${fails.map((name, bounds) => i"$name$bounds")}%\n %"""
117129

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

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

+75-37
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
@@ -2851,7 +2857,16 @@ object TypeComparer {
28512857
comparing(_.tracked(op))
28522858
}
28532859

2860+
object TrackingTypeComparer:
2861+
enum MatchResult:
2862+
case Reduced(tp: Type)
2863+
case Disjoint
2864+
case Stuck
2865+
case NoInstance(fails: List[(Name, TypeBounds)])
2866+
28542867
class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
2868+
import TrackingTypeComparer.*
2869+
28552870
init(initctx)
28562871

28572872
override def trackingTypeComparer = this
@@ -2889,31 +2904,36 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
28892904
}
28902905

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

2902-
def instantiateParams(inst: Array[Type]) = new TypeMap {
2926+
def instantiateParams(insts: Array[Type]) = new ApproximatingTypeMap {
2927+
variance = 0
29032928
def apply(t: Type) = t match {
2904-
case t @ TypeParamRef(b, n) if b `eq` caseLambda => inst(n)
2929+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
29052930
case t: LazyRef => apply(t.ref)
29062931
case _ => mapOver(t)
29072932
}
29082933
}
29092934

2910-
/** Match a single case.
2911-
* @return Some(tp) if the match succeeds with type `tp`
2912-
* Some(NoType) if the match fails, and there is an overlap between pattern and scrutinee
2913-
* None if the match fails and we should consider the following cases
2914-
* because scrutinee and pattern do not overlap
2915-
*/
2916-
def matchCase(cas: Type): Option[Type] = trace(i"match case $cas vs $scrut", matchTypes) {
2935+
/** Match a single case. */
2936+
def matchCase(cas: Type): MatchResult = trace(i"match case $cas vs $scrut", matchTypes) {
29172937
val cas1 = cas match {
29182938
case cas: HKTypeLambda =>
29192939
caseLambda = constrained(cas)
@@ -2924,34 +2944,52 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
29242944

29252945
val defn.MatchCase(pat, body) = cas1: @unchecked
29262946

2927-
if (isSubType(scrut, pat))
2928-
// `scrut` is a subtype of `pat`: *It's a Match!*
2929-
Some {
2930-
caseLambda match {
2931-
case caseLambda: HKTypeLambda =>
2932-
val instances = paramInstances(new Array(caseLambda.paramNames.length), pat)
2933-
instantiateParams(instances)(body).simplified
2934-
case _ =>
2935-
body
2936-
}
2937-
}
2947+
def matches(canWidenAbstract: Boolean): Boolean =
2948+
val saved = this.canWidenAbstract
2949+
this.canWidenAbstract = canWidenAbstract
2950+
try necessarySubType(scrut, pat)
2951+
finally this.canWidenAbstract = saved
2952+
2953+
def redux(canApprox: Boolean): MatchResult =
2954+
caseLambda match
2955+
case caseLambda: HKTypeLambda =>
2956+
val instances = paramInstances(canApprox)(new Array(caseLambda.paramNames.length), pat)
2957+
instantiateParams(instances)(body) match
2958+
case Range(lo, hi) =>
2959+
MatchResult.NoInstance {
2960+
caseLambda.paramNames.zip(instances).collect {
2961+
case (name, Range(lo, hi)) => (name, TypeBounds(lo, hi))
2962+
}
2963+
}
2964+
case redux =>
2965+
MatchResult.Reduced(redux.simplified)
2966+
case _ =>
2967+
MatchResult.Reduced(body)
2968+
2969+
if caseLambda.exists && matches(canWidenAbstract = false) then
2970+
redux(canApprox = true)
2971+
else if matches(canWidenAbstract = true) then
2972+
redux(canApprox = false)
29382973
else if (provablyDisjoint(scrut, pat))
29392974
// We found a proof that `scrut` and `pat` are incompatible.
29402975
// The search continues.
2941-
None
2976+
MatchResult.Disjoint
29422977
else
2943-
Some(NoType)
2978+
MatchResult.Stuck
29442979
}
29452980

29462981
def recur(remaining: List[Type]): Type = remaining match
29472982
case cas :: remaining1 =>
29482983
matchCase(cas) match
2949-
case None =>
2984+
case MatchResult.Disjoint =>
29502985
recur(remaining1)
2951-
case Some(NoType) =>
2986+
case MatchResult.Stuck =>
29522987
MatchTypeTrace.stuck(scrut, cas, remaining1)
29532988
NoType
2954-
case Some(tp) =>
2989+
case MatchResult.NoInstance(fails) =>
2990+
MatchTypeTrace.noInstance(scrut, cas, fails)
2991+
NoType
2992+
case MatchResult.Reduced(tp) =>
29552993
tp
29562994
case Nil =>
29572995
val casesText = MatchTypeTrace.noMatchesText(scrut, cases)

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -5755,7 +5755,13 @@ object Types {
57555755
case _ =>
57565756
scrutinee match
57575757
case Range(lo, hi) => range(bound.bounds.lo, bound.bounds.hi)
5758-
case _ => tp.derivedMatchType(bound, scrutinee, cases)
5758+
case _ =>
5759+
if cases.exists(isRange) then
5760+
Range(
5761+
tp.derivedMatchType(bound, scrutinee, cases.map(lower)),
5762+
tp.derivedMatchType(bound, scrutinee, cases.map(upper)))
5763+
else
5764+
tp.derivedMatchType(bound, scrutinee, cases)
57595765

57605766
override protected def derivedSkolemType(tp: SkolemType, info: Type): Type =
57615767
if info eq tp.info then tp
@@ -5791,7 +5797,7 @@ object Types {
57915797
/** A range of possible types between lower bound `lo` and upper bound `hi`.
57925798
* Only used internally in `ApproximatingTypeMap`.
57935799
*/
5794-
private case class Range(lo: Type, hi: Type) extends UncachedGroundType {
5800+
case class Range(lo: Type, hi: Type) extends UncachedGroundType {
57955801
assert(!lo.isInstanceOf[Range])
57965802
assert(!hi.isInstanceOf[Range])
57975803

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

+1
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

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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
28+
| case Cov[x] => N[x]
29+
| The computed bounds for the parameter are:
30+
| x >: Box[Int]
31+
|
32+
| longer explanation available when compiling with `-explain`

tests/neg/6570-1.scala

+1-1
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

+3-3
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

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Error: tests/neg/i11982.scala:22:38 ---------------------------------------------------------------------------------
2+
22 | val p1: ("msg", 42) = unpair[Tshape] // error: no singleton value for Any
3+
| ^
4+
| No singleton value available for Any.

tests/neg/i11982.scala

+27
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]]]
10+
): Tuple2[Tuple.Head[X], Tuple.Head[Tuple.Tail[X]]] =
11+
type AA = Tuple.Head[X]
12+
type BB = Tuple.Head[Tuple.Tail[X]]
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] // error: no singleton value for Any
23+
24+
@main def pairHello: Unit =
25+
assert(p1 == ("msg", 42))
26+
println(p1)
27+
}

0 commit comments

Comments
 (0)