Skip to content

Commit 104f437

Browse files
authored
Merge pull request #12053 from dotty-staging/fix-12049
Explain match type reduction failures in error messages
2 parents dd6fc82 + f1cb0ba commit 104f437

File tree

14 files changed

+779
-22
lines changed

14 files changed

+779
-22
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package dotty.tools
2+
package dotc
3+
package core
4+
5+
import Types._, Contexts._, Symbols._, Decorators._
6+
import util.Property
7+
8+
/** A utility module to produce match type reduction traces in error messages.
9+
*/
10+
object MatchTypeTrace:
11+
12+
private enum TraceEntry:
13+
case TryReduce(scrut: Type)
14+
case NoMatches(scrut: Type, cases: List[Type])
15+
case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])
16+
import TraceEntry._
17+
18+
private class MatchTrace:
19+
var entries: List[TraceEntry] = Nil
20+
21+
private val MatchTrace = new Property.Key[MatchTrace]
22+
23+
/** Execute `op` and if it involves a failed match type reduction
24+
* return the trace of that reduction. Otherwise return the empty string.
25+
*/
26+
def record(op: Context ?=> Any)(using Context): String =
27+
val trace = new MatchTrace
28+
inContext(ctx.fresh.setProperty(MatchTrace, trace)) {
29+
op
30+
if trace.entries.isEmpty then ""
31+
else
32+
i"""
33+
|
34+
|Note: a match type could not be fully reduced:
35+
|
36+
|${trace.entries.reverse.map(explainEntry)}%\n%"""
37+
}
38+
39+
/** Are we running an operation that records a match type trace? */
40+
def isRecording(using Context): Boolean =
41+
ctx.property(MatchTrace).isDefined
42+
43+
private def matchTypeFail(entry: TraceEntry)(using Context) =
44+
ctx.property(MatchTrace) match
45+
case Some(trace) =>
46+
trace.entries match
47+
case (e: TryReduce) :: es => trace.entries = entry :: trace.entries
48+
case _ =>
49+
case _ =>
50+
51+
/** Record a failure that scrutinee `scrut` does not match any case in `cases`.
52+
* Only the first failure is recorded.
53+
*/
54+
def noMatches(scrut: Type, cases: List[Type])(using Context) =
55+
matchTypeFail(NoMatches(scrut, cases))
56+
57+
/** Record a failure that scrutinee `scrut` does not match `stuckCase` but is
58+
* not disjoint from it either, which means that the remaining cases `otherCases`
59+
* cannot be visited. Only the first failure is recorded.
60+
*/
61+
def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) =
62+
matchTypeFail(Stuck(scrut, stuckCase, otherCases))
63+
64+
/** Record in the trace that we are trying to reduce `scrut` when performing `op`
65+
* If `op` succeeds the entry is removed after exit. If `op` fails, it stays.
66+
*/
67+
def recurseWith(scrut: Type)(op: => Type)(using Context): Type =
68+
ctx.property(MatchTrace) match
69+
case Some(trace) =>
70+
val prev = trace.entries
71+
trace.entries = TryReduce(scrut) :: prev
72+
val res = op
73+
if res.exists then trace.entries = prev
74+
res
75+
case _ =>
76+
op
77+
78+
private def caseText(tp: Type)(using Context): String = tp match
79+
case tp: HKTypeLambda => caseText(tp.resultType)
80+
case defn.MatchCase(pat, body) => i"case $pat => $body"
81+
case _ => i"case $tp"
82+
83+
private def casesText(cases: List[Type])(using Context) =
84+
i"${cases.map(caseText)}%\n %"
85+
86+
private def explainEntry(entry: TraceEntry)(using Context): String = entry match
87+
case TryReduce(scrut: Type) =>
88+
i" trying to reduce $scrut"
89+
case NoMatches(scrut, cases) =>
90+
i""" failed since selector $scrut
91+
| matches none of the cases
92+
|
93+
| ${casesText(cases)}"""
94+
case Stuck(scrut, stuckCase, otherCases) =>
95+
i""" failed since selector $scrut
96+
| does not match ${caseText(stuckCase)}
97+
| and cannot be shown to be disjoint from it either.
98+
| Therefore, reduction cannot advance to the remaining case${if otherCases.length == 1 then "" else "s"}
99+
|
100+
| ${casesText(otherCases)}"""
101+
102+
end MatchTypeTrace
103+

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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,14 @@ class PlainPrinter(_ctx: Context) extends Printer {
178178
case MatchType(bound, scrutinee, cases) =>
179179
changePrec(GlobalPrec) {
180180
def caseText(tp: Type): Text = tp match {
181+
case tp: HKTypeLambda => caseText(tp.resultType)
181182
case defn.MatchCase(pat, body) => "case " ~ toText(pat) ~ " => " ~ toText(body)
182183
case _ => "case " ~ toText(tp)
183184
}
184185
def casesText = Text(cases.map(caseText), "\n")
185-
atPrec(InfixPrec) { toText(scrutinee) } ~
186-
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
187-
(" <: " ~ toText(bound) provided !bound.isAny)
186+
atPrec(InfixPrec) { toText(scrutinee) } ~
187+
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
188+
(" <: " ~ toText(bound) provided !bound.isAny)
188189
}.close
189190
case tp: PreviousErrorType if ctx.settings.XprintTypes.value =>
190191
"<error>" // do not print previously reported error message because they may try to print this error type again recuresevely

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,16 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
105105

106106
override def toTextPrefix(tp: Type): Text = controlled {
107107
def isOmittable(sym: Symbol) =
108-
if (printDebug) false
109-
else if (homogenizedView) isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
108+
if printDebug then false
109+
else if homogenizedView then isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
110+
else if sym.isPackageObject then isOmittablePrefix(sym.owner)
110111
else isOmittablePrefix(sym)
111112
tp match {
112113
case tp: ThisType if isOmittable(tp.cls) =>
113114
""
114115
case tp @ TermRef(pre, _) =>
115116
val sym = tp.symbol
116-
if (sym.isPackageObject && !homogenizedView) toTextPrefix(pre)
117+
if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefix(pre)
117118
else if (isOmittable(sym)) ""
118119
else super.toTextPrefix(tp)
119120
case _ => super.toTextPrefix(tp)
@@ -240,6 +241,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
240241
toTextParents(tp.parents) ~~ "{...}"
241242
case JavaArrayType(elemtp) =>
242243
toText(elemtp) ~ "[]"
244+
case tp: LazyRef if !printDebug =>
245+
try toText(tp.ref)
246+
catch case ex: Throwable => "..."
243247
case tp: SelectionProto =>
244248
"?{ " ~ toText(tp.name) ~
245249
(Str(" ") provided !tp.name.toSimpleName.last.isLetterOrDigit) ~

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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import printing.Formatting
1616
import ErrorMessageID._
1717
import ast.Trees
1818
import config.{Feature, ScalaVersion}
19-
import typer.ErrorReporting.err
19+
import typer.ErrorReporting.{err, matchReductionAddendum}
2020
import typer.ProtoTypes.ViewProto
2121
import scala.util.control.NonFatal
2222
import StdNames.nme
@@ -45,7 +45,11 @@ import transform.SymUtils._
4545
abstract class TypeMsg(errorId: ErrorMessageID) extends Message(errorId):
4646
def kind = "Type"
4747

48-
abstract class TypeMismatchMsg(found: Type, expected: Type)(errorId: ErrorMessageID)(using Context) extends Message(errorId):
48+
trait ShowMatchTrace(tps: Type*)(using Context) extends Message:
49+
override def msgSuffix: String = matchReductionAddendum(tps*)
50+
51+
abstract class TypeMismatchMsg(found: Type, expected: Type)(errorId: ErrorMessageID)(using Context)
52+
extends Message(errorId), ShowMatchTrace(found, expected):
4953
def kind = "Type Mismatch"
5054
def explain = err.whyNoMatchStr(found, expected)
5155
override def canExplain = true
@@ -281,7 +285,7 @@ import transform.SymUtils._
281285
end TypeMismatch
282286

283287
class NotAMember(site: Type, val name: Name, selected: String, addendum: => String = "")(using Context)
284-
extends NotFoundMsg(NotAMemberID) {
288+
extends NotFoundMsg(NotAMemberID), ShowMatchTrace(site) {
285289
//println(i"site = $site, decls = ${site.decls}, source = ${site.typeSymbol.sourceFile}") //DEBUG
286290

287291
def msg = {

compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ object ErrorReporting {
5151
case _ =>
5252
report.error(em"missing arguments for $meth", tree.srcPos)
5353

54+
def matchReductionAddendum(tps: Type*)(using Context): String =
55+
val collectMatchTrace = new TypeAccumulator[String]:
56+
def apply(s: String, tp: Type): String =
57+
if s.nonEmpty then s
58+
else tp match
59+
case tp: AppliedType if tp.isMatchAlias => MatchTypeTrace.record(tp.tryNormalize)
60+
case tp: MatchType => MatchTypeTrace.record(tp.tryNormalize)
61+
case _ => foldOver(s, tp)
62+
tps.foldLeft("")(collectMatchTrace)
63+
5464
class Errors(using Context) {
5565

5666
/** An explanatory note to be added to error messages
@@ -253,7 +263,9 @@ class ImplicitSearchError(
253263
val shortMessage = userDefinedImplicitNotFoundParamMessage
254264
.orElse(userDefinedImplicitNotFoundTypeMessage)
255265
.getOrElse(defaultImplicitNotFoundMessage)
256-
formatMsg(shortMessage)() ++ hiddenImplicitsAddendum
266+
formatMsg(shortMessage)()
267+
++ hiddenImplicitsAddendum
268+
++ ErrorReporting.matchReductionAddendum(pt)
257269
}
258270

259271
private def formatMsg(shortForm: String)(headline: String = shortForm) = arg match {

compiler/test-resources/repl/i5218

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ scala> 0.0 *: tuple
44
val res0: (Double, Int, String, Long) = (0.0,1,2,3)
55
scala> tuple ++ tuple
66
val res1: Int *: String *: Long *:
7-
scala.Tuple.Concat[scala.Tuple$package.EmptyTuple.type, tuple.type] = (1,2,3,1,2,3)
7+
scala.Tuple.Concat[EmptyTuple.type, tuple.type] = (1,2,3,1,2,3)

language-server/test/dotty/tools/languageserver/CompletionTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ class CompletionTest {
4242

4343
@Test def completionFromSyntheticPackageObject: Unit = {
4444
code"class Foo { val foo: IArr${m1} }".withSource
45-
.completion(m1, Set(("IArray", Field, "scala.IArray"),
46-
("IArray", Module, "scala.IArray$package.IArray$")))
45+
.completion(m1, Set(("IArray", Module, "IArray$"),
46+
("IArray", Field, "scala.IArray")))
4747
}
4848

4949
@Test def completionFromJavaDefaults: Unit = {

0 commit comments

Comments
 (0)