Skip to content

Commit

Permalink
splain, take 2
Browse files Browse the repository at this point in the history
  • Loading branch information
tek committed Mar 1, 2020
1 parent 09ad166 commit 6c24ea9
Show file tree
Hide file tree
Showing 13 changed files with 1,172 additions and 8 deletions.
45 changes: 45 additions & 0 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -548,4 +548,49 @@ trait ScalaSettings extends StandardScalaSettings with Warnings {
*/
None
}

object YsplainChoices extends MultiChoiceEnumeration {
val enable = Choice("enable", "activate the splain error formatting engine")
val noColor = Choice("no-color", "don't colorize type errors formatted by splain")
val noBoundsImplicit = Choice("no-bounds-implicit", "suppress any implicit bounds errors")
val noTree = Choice("no-tree", "don't display implicit chains in tree layout")
val verboseTree = Choice("verbose-tree", "display all intermediate implicits in a chain")
val noInfix = Choice("no-infix", "don't format infix types")
val noFoundReq = Choice("no-found-req", "don't format found/required type errors")
}

val Ysplain: MultiChoiceSetting[YsplainChoices.type] =
MultiChoiceSetting(
name = "-Yexplain-implicits",
helpArg = "feature",
descr = "activate the splain error formatter engine",
domain = YsplainChoices,
default = None,
)

val YsplainTruncRefined: IntSetting =
IntSetting(
"-Yexplain-implicits-trunc-refined",
"truncate refined types as F {...}",
0,
Some((0, Int.MaxValue)),
str => Some(str.toInt),
)

val YsplainBreakInfix: IntSetting =
IntSetting(
"-Yexplain-implicits-break-infix",
"break infix types into multiple lines when exceeding this number of characters",
0,
Some((0, Int.MaxValue)),
str => Some(str.toInt),
)

def splainSettingEnable: Boolean = Ysplain.contains(YsplainChoices.enable)
def splainSettingNoFoundReq: Boolean = Ysplain.contains(YsplainChoices.noFoundReq)
def splainSettingNoInfix: Boolean =Ysplain.contains(YsplainChoices.noInfix)
def splainSettingNoColor: Boolean = Ysplain.contains(YsplainChoices.noColor)
def splainSettingVerboseTree: Boolean = Ysplain.contains(YsplainChoices.verboseTree)
def splainSettingNoTree: Boolean = Ysplain.contains(YsplainChoices.noTree)
def splainSettingNoBoundsImplicits: Boolean = Ysplain.contains(YsplainChoices.noBoundsImplicit)
}
18 changes: 17 additions & 1 deletion src/compiler/scala/tools/nsc/typechecker/AnalyzerPlugins.scala
Expand Up @@ -16,7 +16,7 @@ package typechecker
/**
* @author Lukas Rytz
*/
trait AnalyzerPlugins { self: Analyzer =>
trait AnalyzerPlugins { self: Analyzer with splain.SplainData =>
import global._

trait AnalyzerPlugin {
Expand Down Expand Up @@ -179,6 +179,15 @@ trait AnalyzerPlugins { self: Analyzer =>
* @param result The result to a given implicit search.
*/
def pluginsNotifyImplicitSearchResult(result: SearchResult): Unit = ()

/**
* Construct a custom error message for implicit parameters that could not be resolved.
*
* @param tree The tree that requested the implicit
* @param param The implicit parameter that was resolved
*/
def noImplicitFoundError(param: Symbol, errors: List[ImplicitError], previous: Option[String]): Option[String] =
previous
}

/**
Expand Down Expand Up @@ -390,6 +399,13 @@ trait AnalyzerPlugins { self: Analyzer =>
def accumulate = (_, p) => p.pluginsNotifyImplicitSearchResult(result)
})

/** @see AnalyzerPlugin.noImplicitFoundError */
def pluginsNoImplicitFoundError(param: Symbol, errors: List[ImplicitError], initial: String): Option[String] =
invoke(new CumulativeOp[Option[String]] {
def default = Some(initial)
def accumulate = (previous, p) => p.noImplicitFoundError(param, errors, previous)
})

/** A list of registered macro plugins */
private var macroPlugins: List[MacroPlugin] = Nil

Expand Down
9 changes: 6 additions & 3 deletions src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala
Expand Up @@ -23,7 +23,9 @@ import scala.tools.nsc.util.stackTraceString
import scala.reflect.io.NoAbstractFile
import scala.reflect.internal.util.NoSourceFile

trait ContextErrors {
trait ContextErrors
extends splain.SplainErrors
{
self: Analyzer =>

import global._
Expand Down Expand Up @@ -151,7 +153,7 @@ trait ContextErrors {
MacroIncompatibleEngineError("macro cannot be expanded, because it was compiled by an incompatible macro engine", internalMessage)

def NoImplicitFoundError(tree: Tree, param: Symbol)(implicit context: Context): Unit = {
def errMsg = {
def defaultErrMsg = {
val paramName = param.name
val paramTp = param.tpe
def evOrParam =
Expand All @@ -173,7 +175,8 @@ trait ContextErrors {
}
}
}
issueNormalTypeError(tree, errMsg)
val errMsg = splainPushOrReportNotFound(tree, param)
issueNormalTypeError(tree, errMsg.getOrElse(defaultErrMsg))
}

trait TyperContextErrors {
Expand Down
8 changes: 6 additions & 2 deletions src/compiler/scala/tools/nsc/typechecker/Implicits.scala
Expand Up @@ -32,7 +32,7 @@ import scala.language.implicitConversions
*
* @author Martin Odersky
*/
trait Implicits {
trait Implicits extends splain.SplainData {
self: Analyzer =>

import global._
Expand Down Expand Up @@ -105,12 +105,14 @@ trait Implicits {
if (shouldPrint)
typingStack.printTyping(tree, "typing implicit: %s %s".format(tree, context.undetparamsString))
val implicitSearchContext = context.makeImplicit(reportAmbiguous)
ImplicitErrors.startSearch(pt)
val dpt = if (isView) pt else dropByName(pt)
val isByName = dpt ne pt
val search = new ImplicitSearch(tree, dpt, isView, implicitSearchContext, pos, isByName)
pluginsNotifyImplicitSearch(search)
val result = search.bestImplicit
pluginsNotifyImplicitSearchResult(result)
ImplicitErrors.finishSearch(result.isSuccess, pt)

if (result.isFailure && saveAmbiguousDivergent && implicitSearchContext.reporter.hasErrors)
implicitSearchContext.reporter.propagateImplicitTypeErrorsTo(context.reporter)
Expand Down Expand Up @@ -909,7 +911,8 @@ trait Implicits {
// bounds check on the expandee tree
itree3.attachments.get[MacroExpansionAttachment] match {
case Some(MacroExpansionAttachment(exp @ TypeApply(fun, targs), _)) =>
checkBounds(exp, NoPrefix, NoSymbol, fun.symbol.typeParams, targs.map(_.tpe), "inferred ")
val withinBounds = checkBounds(exp, NoPrefix, NoSymbol, fun.symbol.typeParams, targs.map(_.tpe), "inferred ")
if (!withinBounds) splainPushNonconformantBonds(pt, tree, targs.map(_.tpe), undetParams, None)
case _ => ()
}

Expand Down Expand Up @@ -956,6 +959,7 @@ trait Implicits {

context.reporter.firstError match {
case Some(err) =>
splainPushImplicitSearchFailure(itree3, pt, err)
fail("typing TypeApply reported errors for the implicit tree: " + err.errMsg)
case None =>
val result = new SearchResult(unsuppressMacroExpansion(itree3), subst, context.undetparams)
Expand Down
Expand Up @@ -39,7 +39,9 @@ import scala.annotation.tailrec
*
* @author Paul Phillips
*/
trait TypeDiagnostics {
trait TypeDiagnostics
extends splain.SplainDiagnostics
{
self: Analyzer with StdAttachments =>

import global._
Expand Down Expand Up @@ -308,7 +310,7 @@ trait TypeDiagnostics {
// when the message will never be seen. I though context.reportErrors
// being false would do that, but if I return "<suppressed>" under
// that condition, I see it.
def foundReqMsg(found: Type, req: Type): String = {
def builtinFoundReqMsg(found: Type, req: Type): String = {
val foundWiden = found.widen
val reqWiden = req.widen
val sameNamesDifferentPrefixes =
Expand Down Expand Up @@ -338,6 +340,9 @@ trait TypeDiagnostics {
}
}

def foundReqMsg(found: Type, req: Type): String =
splainFoundReqMsg(found, req).getOrElse(builtinFoundReqMsg(found, req))

def typePatternAdvice(sym: Symbol, ptSym: Symbol) = {
val clazz = if (sym.isModuleClass) sym.companionClass else sym
val caseString =
Expand Down
47 changes: 47 additions & 0 deletions src/compiler/scala/tools/nsc/typechecker/splain/Colors.scala
@@ -0,0 +1,47 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.tools.nsc
package typechecker
package splain

trait StringColor
{
def color(s: String, col: String): String
}

object StringColors
{
implicit val noColor =
new StringColor {
def color(s: String, col: String) = s
}

implicit val color =
new StringColor {
import Console.RESET

def color(s: String, col: String) = col + s + RESET
}
}

object StringColor
{
implicit class StringColorOps(s: String)(implicit sc: StringColor)
{
import Console._
def red = sc.color(s, RED)
def green = sc.color(s, GREEN)
def yellow = sc.color(s, YELLOW)
def blue = sc.color(s, BLUE)
}
}
107 changes: 107 additions & 0 deletions src/compiler/scala/tools/nsc/typechecker/splain/SplainData.scala
@@ -0,0 +1,107 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.tools.nsc
package typechecker
package splain

import scala.util.matching.Regex

trait SplainData { self: Analyzer =>

import global._

sealed trait ImplicitErrorSpecifics

object ImplicitErrorSpecifics
{
case class NotFound(param: Symbol)
extends ImplicitErrorSpecifics

case class NonconformantBounds(targs: List[Type], tparams: List[Symbol], originalError: Option[AbsTypeError])
extends ImplicitErrorSpecifics
}

object ImplicitErrors
{
var stack: List[Type] = Nil

var errors: List[ImplicitError] = Nil

def push(error: ImplicitError): Unit = errors = error :: errors

def nesting: Int = stack.length - 1

def nested: Boolean = stack.nonEmpty

def removeErrorsFor(tpe: Type): Unit = errors = errors.dropWhile(_.tpe == tpe)

def startSearch(expectedType: Type): Unit = {
if (!nested) errors = List()
stack = expectedType :: stack
}

def finishSearch(success: Boolean, expectedType: Type): Unit = {
if (success) removeErrorsFor(expectedType)
stack = stack.drop(1)
}
}

case class ImplicitError(tpe: Type, candidate: Tree, nesting: Int, specifics: ImplicitErrorSpecifics)
{
override def equals(other: Any) = other match {
case o: ImplicitError =>
o.tpe.toString == tpe.toString && ImplicitError.candidateName(this) == ImplicitError.candidateName(o)
case _ => false
}

override def hashCode = (tpe.toString.hashCode, ImplicitError.candidateName(this).hashCode).hashCode

override def toString: String =
s"NotFound(${ImplicitError.shortName(tpe.toString)}, ${ImplicitError.shortName(candidate.toString)}), $nesting, $specifics)"
}

object ImplicitError
{
def notFound(tpe: Type, candidate: Tree, nesting: Int)(param: Symbol): ImplicitError =
ImplicitError(tpe, candidate, nesting, ImplicitErrorSpecifics.NotFound(param))

def nonconformantBounds
(tpe: Type, candidate: Tree, nesting: Int)
(targs: List[Type], tparams: List[Symbol], originalError: Option[AbsTypeError])
: ImplicitError =
ImplicitError(tpe, candidate, nesting, ImplicitErrorSpecifics.NonconformantBounds(targs, tparams, originalError))

def unapplyCandidate(e: ImplicitError): Tree =
e.candidate match {
case TypeApply(name, _) => name
case a => a
}

def candidateName(e: ImplicitError): String =
unapplyCandidate(e) match {
case Select(_, name) => name.toString
case Ident(name) => name.toString
case a => a.toString
}

val candidateRegex: Regex = """.*\.this\.(.*)""".r

def cleanCandidate(e: ImplicitError): String =
unapplyCandidate(e).toString match {
case candidateRegex(suf) => suf
case a => a
}

def shortName(ident: String): String = ident.split('.').toList.lastOption.getOrElse(ident)
}
}
@@ -0,0 +1,42 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.tools.nsc
package typechecker
package splain

import scala.util.control.NonFatal

trait SplainDiagnostics
extends SplainFormatting
{ self: Analyzer with SplainData =>
import global._

def showStats[A](desc: String, run: => A): A = {
val ret = run
if (sys.env.contains("SPLAIN_CACHE_STATS"))
reporter.echo(s"$desc entries/hits: $cacheStats")
ret
}

def foundReqMsgShort(found: Type, req: Type): Option[TypeRepr] =
try {
Some(showStats("foundreq", showFormattedL(formatDiff(found, req, true), true)))
} catch {
case NonFatal(e) =>
None
}

def splainFoundReqMsg(found: Type, req: Type): Option[String] =
if (!settings.splainSettingEnable || settings.splainSettingNoFoundReq) None
else foundReqMsgShort(found, req).map(a => ";\n" + a.indent.joinLines)
}

0 comments on commit 6c24ea9

Please sign in to comment.