Skip to content

Explain match type reduction failures in error messages #12053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions compiler/src/dotty/tools/dotc/core/MatchTypeTrace.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dotty.tools
package dotc
package core

import Types._, Contexts._, Symbols._, Decorators._
import util.Property

/** A utility module to produce match type reduction traces in error messages.
*/
object MatchTypeTrace:

private enum TraceEntry:
case TryReduce(scrut: Type)
case NoMatches(scrut: Type, cases: List[Type])
case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])
import TraceEntry._

private class MatchTrace:
var entries: List[TraceEntry] = Nil

private val MatchTrace = new Property.Key[MatchTrace]

/** Execute `op` and if it involves a failed match type reduction
* return the trace of that reduction. Otherwise return the empty string.
*/
def record(op: Context ?=> Any)(using Context): String =
val trace = new MatchTrace
inContext(ctx.fresh.setProperty(MatchTrace, trace)) {
op
if trace.entries.isEmpty then ""
else
i"""
|
|Note: a match type could not be fully reduced:
|
|${trace.entries.reverse.map(explainEntry)}%\n%"""
}

/** Are we running an operation that records a match type trace? */
def isRecording(using Context): Boolean =
ctx.property(MatchTrace).isDefined

private def matchTypeFail(entry: TraceEntry)(using Context) =
ctx.property(MatchTrace) match
case Some(trace) =>
trace.entries match
case (e: TryReduce) :: es => trace.entries = entry :: trace.entries
case _ =>
case _ =>

/** Record a failure that scrutinee `scrut` does not match any case in `cases`.
* Only the first failure is recorded.
*/
def noMatches(scrut: Type, cases: List[Type])(using Context) =
matchTypeFail(NoMatches(scrut, cases))

/** Record a failure that scrutinee `scrut` does not match `stuckCase` but is
* not disjoint from it either, which means that the remaining cases `otherCases`
* cannot be visited. Only the first failure is recorded.
*/
def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) =
matchTypeFail(Stuck(scrut, stuckCase, otherCases))

/** Record in the trace that we are trying to reduce `scrut` when performing `op`
* If `op` succeeds the entry is removed after exit. If `op` fails, it stays.
*/
def recurseWith(scrut: Type)(op: => Type)(using Context): Type =
ctx.property(MatchTrace) match
case Some(trace) =>
val prev = trace.entries
trace.entries = TryReduce(scrut) :: prev
val res = op
if res.exists then trace.entries = prev
res
case _ =>
op

private def caseText(tp: Type)(using Context): String = tp match
case tp: HKTypeLambda => caseText(tp.resultType)
case defn.MatchCase(pat, body) => i"case $pat => $body"
case _ => i"case $tp"

private def casesText(cases: List[Type])(using Context) =
i"${cases.map(caseText)}%\n %"

private def explainEntry(entry: TraceEntry)(using Context): String = entry match
case TryReduce(scrut: Type) =>
i" trying to reduce $scrut"
case NoMatches(scrut, cases) =>
i""" failed since selector $scrut
| matches none of the cases
|
| ${casesText(cases)}"""
case Stuck(scrut, stuckCase, otherCases) =>
i""" failed since selector $scrut
| does not match ${caseText(stuckCase)}
| and cannot be shown to be disjoint from it either.
| Therefore, reduction cannot advance to the remaining case${if otherCases.length == 1 then "" else "s"}
|
| ${casesText(otherCases)}"""

end MatchTypeTrace

18 changes: 14 additions & 4 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2798,10 +2798,20 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
Some(NoType)
}

def recur(cases: List[Type]): Type = cases match {
case cas :: cases1 => matchCase(cas).getOrElse(recur(cases1))
case Nil => NoType
}
def recur(remaining: List[Type]): Type = remaining match
case cas :: remaining1 =>
matchCase(cas) match
case None =>
recur(remaining1)
case Some(NoType) =>
if remaining1.isEmpty then MatchTypeTrace.noMatches(scrut, cases)
else MatchTypeTrace.stuck(scrut, cas, remaining1)
NoType
case Some(tp) =>
tp
case Nil =>
MatchTypeTrace.noMatches(scrut, cases)
NoType

inFrozenConstraint {
// Empty types break the basic assumption that if a scrutinee and a
Expand Down
11 changes: 8 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4025,7 +4025,9 @@ object Types {
def tryMatchAlias = tycon.info match {
case MatchAlias(alias) =>
trace(i"normalize $this", typr, show = true) {
alias.applyIfParameterized(args).tryNormalize
MatchTypeTrace.recurseWith(this) {
alias.applyIfParameterized(args).tryNormalize
}
}
case _ =>
NoType
Expand Down Expand Up @@ -4537,7 +4539,11 @@ object Types {
}

record("MatchType.reduce called")
if (!Config.cacheMatchReduced || myReduced == null || !isUpToDate) {
if !Config.cacheMatchReduced
|| myReduced == null
|| !isUpToDate
|| MatchTypeTrace.isRecording
then
record("MatchType.reduce computed")
if (myReduced != null) record("MatchType.reduce cache miss")
myReduced =
Expand All @@ -4549,7 +4555,6 @@ object Types {
finally updateReductionContext(cmp.footprint)
TypeComparer.tracked(matchCases)
}
}
myReduced
}

Expand Down
7 changes: 4 additions & 3 deletions compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,14 @@ class PlainPrinter(_ctx: Context) extends Printer {
case MatchType(bound, scrutinee, cases) =>
changePrec(GlobalPrec) {
def caseText(tp: Type): Text = tp match {
case tp: HKTypeLambda => caseText(tp.resultType)
case defn.MatchCase(pat, body) => "case " ~ toText(pat) ~ " => " ~ toText(body)
case _ => "case " ~ toText(tp)
}
def casesText = Text(cases.map(caseText), "\n")
atPrec(InfixPrec) { toText(scrutinee) } ~
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
(" <: " ~ toText(bound) provided !bound.isAny)
atPrec(InfixPrec) { toText(scrutinee) } ~
keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~
(" <: " ~ toText(bound) provided !bound.isAny)
}.close
case tp: PreviousErrorType if ctx.settings.XprintTypes.value =>
"<error>" // do not print previously reported error message because they may try to print this error type again recuresevely
Expand Down
10 changes: 7 additions & 3 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,16 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {

override def toTextPrefix(tp: Type): Text = controlled {
def isOmittable(sym: Symbol) =
if (printDebug) false
else if (homogenizedView) isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
if printDebug then false
else if homogenizedView then isEmptyPrefix(sym) // drop <root> and anonymous classes, but not scala, Predef.
else if sym.isPackageObject then isOmittablePrefix(sym.owner)
else isOmittablePrefix(sym)
tp match {
case tp: ThisType if isOmittable(tp.cls) =>
""
case tp @ TermRef(pre, _) =>
val sym = tp.symbol
if (sym.isPackageObject && !homogenizedView) toTextPrefix(pre)
if sym.isPackageObject && !homogenizedView && !printDebug then toTextPrefix(pre)
else if (isOmittable(sym)) ""
else super.toTextPrefix(tp)
case _ => super.toTextPrefix(tp)
Expand Down Expand Up @@ -240,6 +241,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
toTextParents(tp.parents) ~~ "{...}"
case JavaArrayType(elemtp) =>
toText(elemtp) ~ "[]"
case tp: LazyRef if !printDebug =>
try toText(tp.ref)
catch case ex: Throwable => "..."
case tp: SelectionProto =>
"?{ " ~ toText(tp.name) ~
(Str(" ") provided !tp.name.toSimpleName.last.isLetterOrDigit) ~
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/Message.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
*/
protected def explain: String

/** A message suffix that can be added for certain subclasses */
protected def msgSuffix: String = ""

/** Does this message have an explanation?
* This is normally the same as `explain.nonEmpty` but can be overridden
* if we need a way to return `true` without actually calling the
Expand All @@ -82,7 +85,7 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
def rawMessage = message

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

/** The explanation to report. <nonsensical> tags are filtered out */
lazy val explanation: String = dropNonSensical(explain)
Expand Down
10 changes: 7 additions & 3 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import printing.Formatting
import ErrorMessageID._
import ast.Trees
import config.{Feature, ScalaVersion}
import typer.ErrorReporting.err
import typer.ErrorReporting.{err, matchReductionAddendum}
import typer.ProtoTypes.ViewProto
import scala.util.control.NonFatal
import StdNames.nme
Expand Down Expand Up @@ -45,7 +45,11 @@ import transform.SymUtils._
abstract class TypeMsg(errorId: ErrorMessageID) extends Message(errorId):
def kind = "Type"

abstract class TypeMismatchMsg(found: Type, expected: Type)(errorId: ErrorMessageID)(using Context) extends Message(errorId):
trait ShowMatchTrace(tps: Type*)(using Context) extends Message:
override def msgSuffix: String = matchReductionAddendum(tps*)

abstract class TypeMismatchMsg(found: Type, expected: Type)(errorId: ErrorMessageID)(using Context)
extends Message(errorId), ShowMatchTrace(found, expected):
def kind = "Type Mismatch"
def explain = err.whyNoMatchStr(found, expected)
override def canExplain = true
Expand Down Expand Up @@ -281,7 +285,7 @@ import transform.SymUtils._
end TypeMismatch

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

def msg = {
Expand Down
14 changes: 13 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ object ErrorReporting {
case _ =>
report.error(em"missing arguments for $meth", tree.srcPos)

def matchReductionAddendum(tps: Type*)(using Context): String =
val collectMatchTrace = new TypeAccumulator[String]:
def apply(s: String, tp: Type): String =
if s.nonEmpty then s
else tp match
case tp: AppliedType if tp.isMatchAlias => MatchTypeTrace.record(tp.tryNormalize)
case tp: MatchType => MatchTypeTrace.record(tp.tryNormalize)
case _ => foldOver(s, tp)
tps.foldLeft("")(collectMatchTrace)

class Errors(using Context) {

/** An explanatory note to be added to error messages
Expand Down Expand Up @@ -253,7 +263,9 @@ class ImplicitSearchError(
val shortMessage = userDefinedImplicitNotFoundParamMessage
.orElse(userDefinedImplicitNotFoundTypeMessage)
.getOrElse(defaultImplicitNotFoundMessage)
formatMsg(shortMessage)() ++ hiddenImplicitsAddendum
formatMsg(shortMessage)()
++ hiddenImplicitsAddendum
++ ErrorReporting.matchReductionAddendum(pt)
}

private def formatMsg(shortForm: String)(headline: String = shortForm) = arg match {
Expand Down
2 changes: 1 addition & 1 deletion compiler/test-resources/repl/i5218
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ scala> 0.0 *: tuple
val res0: (Double, Int, String, Long) = (0.0,1,2,3)
scala> tuple ++ tuple
val res1: Int *: String *: Long *:
scala.Tuple.Concat[scala.Tuple$package.EmptyTuple.type, tuple.type] = (1,2,3,1,2,3)
scala.Tuple.Concat[EmptyTuple.type, tuple.type] = (1,2,3,1,2,3)
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class CompletionTest {

@Test def completionFromSyntheticPackageObject: Unit = {
code"class Foo { val foo: IArr${m1} }".withSource
.completion(m1, Set(("IArray", Field, "scala.IArray"),
("IArray", Module, "scala.IArray$package.IArray$")))
.completion(m1, Set(("IArray", Module, "IArray$"),
("IArray", Field, "scala.IArray")))
}

@Test def completionFromJavaDefaults: Unit = {
Expand Down
Loading