Skip to content

Commit 60b513d

Browse files
committed
Explain match type reduction failures in error messages
1 parent b44cafa commit 60b513d

File tree

7 files changed

+133
-11
lines changed

7 files changed

+133
-11
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package dotty.tools
2+
package dotc
3+
package core
4+
5+
import Types._, Contexts._, Symbols._, Decorators._
6+
import util.Property
7+
8+
object MatchTypeTrace:
9+
private enum TraceEntry:
10+
case TryReduce(scrut: Type)
11+
case NoMatches(scrut: Type, cases: List[Type])
12+
case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])
13+
import TraceEntry._
14+
15+
private class MatchTrace:
16+
var entries: List[TraceEntry] = Nil
17+
18+
private val MatchTrace = new Property.Key[MatchTrace]
19+
20+
def record(op: Context ?=> Any)(using Context): String =
21+
val trace = new MatchTrace
22+
inContext(ctx.fresh.setProperty(MatchTrace, trace)) {
23+
op
24+
if trace.entries.isEmpty then ""
25+
else
26+
i"""
27+
|
28+
|Note: a match type could not be fully reduced:
29+
|
30+
|${trace.entries.reverse.map(explainEntry)}%\n%"""
31+
}
32+
33+
def isRecording(using Context): Boolean =
34+
ctx.property(MatchTrace).isDefined
35+
36+
private def matchTypeFail(entry: TraceEntry)(using Context) =
37+
ctx.property(MatchTrace) match
38+
case Some(trace) =>
39+
trace.entries match
40+
case (e: TryReduce) :: es => trace.entries = entry :: trace.entries
41+
case _ =>
42+
case _ =>
43+
44+
def noMatches(scrut: Type, cases: List[Type])(using Context) =
45+
matchTypeFail(NoMatches(scrut, cases))
46+
47+
def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) =
48+
matchTypeFail(Stuck(scrut, stuckCase, otherCases))
49+
50+
def recurseWith(scrut: Type)(op: => Type)(using Context): Type =
51+
ctx.property(MatchTrace) match
52+
case Some(trace) =>
53+
val prev = trace.entries
54+
trace.entries = TryReduce(scrut) :: prev
55+
val res = op
56+
if res.exists then trace.entries = prev
57+
res
58+
case _ =>
59+
op
60+
61+
private def caseText(tp: Type)(using Context): String = tp match
62+
case tp: HKTypeLambda => caseText(tp.resultType)
63+
case defn.MatchCase(pat, body) => i"case $pat => $body"
64+
case _ => i"case $tp"
65+
66+
private def casesText(cases: List[Type])(using Context) =
67+
i"${cases.map(caseText)}%\n %"
68+
69+
private def explainEntry(entry: TraceEntry)(using Context): String = entry match
70+
case TryReduce(scrut: Type) =>
71+
i" trying to reduce $scrut"
72+
case NoMatches(scrut, cases) =>
73+
i""" failed since selector $scrut
74+
| matches none of the cases
75+
|
76+
| ${casesText(cases)}"""
77+
case Stuck(scrut, stuckCase, otherCases) =>
78+
i""" failed since selector $scrut
79+
| does not match ${caseText(stuckCase)}
80+
| and cannot be shown to be disjoint from it either.
81+
| Therefore, reduction cannot advance to the remaining case${if otherCases.length == 1 then "" else "s"}
82+
|
83+
| ${casesText(otherCases)}"""
84+
85+
end MatchTypeTrace
86+

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2798,10 +2798,20 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
27982798
Some(NoType)
27992799
}
28002800

2801-
def recur(cases: List[Type]): Type = cases match {
2802-
case cas :: cases1 => matchCase(cas).getOrElse(recur(cases1))
2803-
case Nil => NoType
2804-
}
2801+
def recur(remaining: List[Type]): Type = remaining match
2802+
case cas :: remaining1 =>
2803+
matchCase(cas) match
2804+
case None =>
2805+
recur(remaining1)
2806+
case Some(NoType) =>
2807+
if remaining1.isEmpty then MatchTypeTrace.noMatches(scrut, cases)
2808+
else MatchTypeTrace.stuck(scrut, cas, remaining1)
2809+
NoType
2810+
case Some(tp) =>
2811+
tp
2812+
case Nil =>
2813+
MatchTypeTrace.noMatches(scrut, cases)
2814+
NoType
28052815

28062816
inFrozenConstraint {
28072817
// Empty types break the basic assumption that if a scrutinee and a

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4025,7 +4025,9 @@ object Types {
40254025
def tryMatchAlias = tycon.info match {
40264026
case MatchAlias(alias) =>
40274027
trace(i"normalize $this", typr, show = true) {
4028-
alias.applyIfParameterized(args).tryNormalize
4028+
MatchTypeTrace.recurseWith(this) {
4029+
alias.applyIfParameterized(args).tryNormalize
4030+
}
40294031
}
40304032
case _ =>
40314033
NoType
@@ -4537,7 +4539,11 @@ object Types {
45374539
}
45384540

45394541
record("MatchType.reduce called")
4540-
if (!Config.cacheMatchReduced || myReduced == null || !isUpToDate) {
4542+
if !Config.cacheMatchReduced
4543+
|| myReduced == null
4544+
|| !isUpToDate
4545+
|| MatchTypeTrace.isRecording
4546+
then
45414547
record("MatchType.reduce computed")
45424548
if (myReduced != null) record("MatchType.reduce cache miss")
45434549
myReduced =
@@ -4549,7 +4555,6 @@ object Types {
45494555
finally updateReductionContext(cmp.footprint)
45504556
TypeComparer.tracked(matchCases)
45514557
}
4552-
}
45534558
myReduced
45544559
}
45554560

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ class PlainPrinter(_ctx: Context) extends Printer {
182182
case _ => "case " ~ toText(tp)
183183
}
184184
def casesText = Text(cases.map(caseText), "\n")
185-
atPrec(InfixPrec) { toText(scrutinee) } ~
186-
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
187-
(" <: " ~ toText(bound) provided !bound.isAny)
185+
atPrec(InfixPrec) { toText(scrutinee) } ~
186+
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
187+
(" <: " ~ toText(bound) provided !bound.isAny)
188188
}.close
189189
case tp: PreviousErrorType if ctx.settings.XprintTypes.value =>
190190
"<error>" // do not print previously reported error message because they may try to print this error type again recuresevely

compiler/src/dotty/tools/dotc/reporting/Message.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
5858
*/
5959
protected def explain: String
6060

61+
/** A message suffix that can be added for certain subclasses */
62+
protected def msgSuffix: String = ""
63+
6164
/** Does this message have an explanation?
6265
* This is normally the same as `explain.nonEmpty` but can be overridden
6366
* if we need a way to return `true` without actually calling the
@@ -82,7 +85,7 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
8285
def rawMessage = message
8386

8487
/** The message to report. <nonsensical> tags are filtered out */
85-
lazy val message: String = dropNonSensical(msg)
88+
lazy val message: String = dropNonSensical(msg + msgSuffix)
8689

8790
/** The explanation to report. <nonsensical> tags are filtered out */
8891
lazy val explanation: String = dropNonSensical(explain)

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ import transform.SymUtils._
5050
def explain = err.whyNoMatchStr(found, expected)
5151
override def canExplain = true
5252

53+
override def msgSuffix: String =
54+
val collectMatchTrace = new TypeAccumulator[String]:
55+
def apply(s: String, tp: Type): String =
56+
if s.nonEmpty then s
57+
else tp match
58+
case tp: AppliedType if tp.isMatchAlias => MatchTypeTrace.record(tp.tryNormalize)
59+
case tp: MatchType => MatchTypeTrace.record(tp.tryNormalize)
60+
case _ => foldOver(s, tp)
61+
collectMatchTrace(collectMatchTrace("", found), expected)
62+
end TypeMismatchMsg
63+
5364
abstract class NamingMsg(errorId: ErrorMessageID) extends Message(errorId):
5465
def kind = "Naming"
5566

tests/neg/i12049.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
trait A
2+
trait B
3+
type M[X] = X match
4+
case A => Int
5+
case B => String
6+
val x: String = ??? : M[B]
7+

0 commit comments

Comments
 (0)