Skip to content

Commit

Permalink
Merge pull request #13886 from dotty-staging/fix-13838
Browse files Browse the repository at this point in the history
Impose implicit search limit
  • Loading branch information
odersky committed Nov 9, 2021
2 parents 5ce25fe + 739dec3 commit aa25df2
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 43 deletions.
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Expand Up @@ -218,6 +218,7 @@ private sealed trait XSettings:
val Xtarget: Setting[String] = ChoiceSetting("-Xtarget", "target", "Emit bytecode for the specified version of the Java platform. This might produce bytecode that will break at runtime. When on JDK 9+, consider -release as a safer alternative.", ScalaSettings.supportedTargetVersions, "", aliases = List("--Xtarget"))
val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))
val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d <jar>)", "")
val XimplicitSearchLimit: Setting[Int] = IntSetting("-Ximplicit-search-limit", "Maximal number of expressions to be generated in an implicit search", 50000)

val XmixinForceForwarders = ChoiceSetting(
name = "-Xmixin-force-forwarders",
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala
Expand Up @@ -175,7 +175,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID]:
OverrideErrorID,
MatchableWarningID,
CannotExtendFunctionID,
LossyWideningConstantConversionID
LossyWideningConstantConversionID,
ImplicitSearchTooLargeID

def errorNumber = ordinal - 2

Expand Down
7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/Message.scala
Expand Up @@ -124,6 +124,13 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
def explain = self.explain ++ suffix
override def canExplain = true

/** Override with `true` for messages that should always be shown even if their
* position overlaps another messsage of a different class. On the other hand
* multiple messages of the same class with overlapping positions will lead
* to only a single message of that class to be issued.
*/
def showAlways = false

override def toString = msg
}

Expand Down
Expand Up @@ -10,19 +10,24 @@ import core.Contexts._
* are suppressed, unless they are of increasing severity. */
trait UniqueMessagePositions extends Reporter {

private val positions = new mutable.HashMap[(SourceFile, Int), Int]
private val positions = new mutable.HashMap[(SourceFile, Int), Diagnostic]

/** Logs a position and returns true if it was already logged.
* @note Two positions are considered identical for logging if they have the same point.
*/
override def isHidden(dia: Diagnostic)(using Context): Boolean =
extension (dia1: Diagnostic) def hides(dia2: Diagnostic): Boolean =
if dia2.msg.showAlways then dia1.msg.getClass == dia2.msg.getClass
else dia1.level >= dia2.level
super.isHidden(dia) || {
dia.pos.exists && !ctx.settings.YshowSuppressedErrors.value && {
dia.pos.exists
&& !ctx.settings.YshowSuppressedErrors.value
&& {
var shouldHide = false
for (pos <- dia.pos.start to dia.pos.end)
positions get (ctx.source, pos) match {
case Some(level) if level >= dia.level => shouldHide = true
case _ => positions((ctx.source, pos)) = dia.level
case Some(dia1) if dia1.hides(dia) => shouldHide = true
case _ => positions((ctx.source, pos)) = dia
}
shouldHide
}
Expand Down
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Expand Up @@ -18,6 +18,7 @@ import ast.Trees
import config.{Feature, ScalaVersion}
import typer.ErrorReporting.{err, matchReductionAddendum}
import typer.ProtoTypes.ViewProto
import typer.Implicits.Candidate
import scala.util.control.NonFatal
import StdNames.nme
import printing.Formatting.hl
Expand Down Expand Up @@ -2515,3 +2516,26 @@ import transform.SymUtils._
|Inlining such definition would multiply this footprint for each call site.
|""".stripMargin
}

class ImplicitSearchTooLargeWarning(limit: Int, openSearchPairs: List[(Candidate, Type)])(using Context)
extends TypeMsg(ImplicitSearchTooLargeID):
override def showAlways = true
def showQuery(query: (Candidate, Type)): String =
i" ${query._1.ref.symbol.showLocated} for ${query._2}}"
def msg =
em"""Implicit search problem too large.
|an implicit search was terminated with failure after trying $limit expressions.
|The root candidate for the search was:
|
|${showQuery(openSearchPairs.last)}
|
|You can change the behavior by setting the `-Ximplicit-search-limit` value.
|Smaller values cause the search to fail faster.
|Larger values might make a very large search problem succeed.
|"""
def explain =
em"""The overflow happened with the following lists of tried expressions and target types,
|starting with the root query:
|
|${openSearchPairs.reverse.map(showQuery)}%\n%
"""
107 changes: 71 additions & 36 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Expand Up @@ -489,6 +489,11 @@ object Implicits:
@sharable val NoMatchingImplicitsFailure: SearchFailure =
SearchFailure(NoMatchingImplicits, NoSpan)(using NoContext)

@sharable object ImplicitSearchTooLarge extends NoMatchingImplicits(NoType, EmptyTree, OrderingConstraint.empty)

@sharable val ImplicitSearchTooLargeFailure: SearchFailure =
SearchFailure(ImplicitSearchTooLarge, NoSpan)(using NoContext)

/** An ambiguous implicits failure */
class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree) extends SearchFailureType {
def explanation(using Context): String =
Expand Down Expand Up @@ -790,16 +795,8 @@ trait Implicits:
*/
def inferView(from: Tree, to: Type)(using Context): SearchResult = {
record("inferView")
val wfromtp = from.tpe.widen
if to.isAny
|| to.isAnyRef
|| to.isRef(defn.UnitClass)
|| wfromtp.isRef(defn.NothingClass)
|| wfromtp.isRef(defn.NullClass)
|| !ctx.mode.is(Mode.ImplicitsEnabled)
|| from.isInstanceOf[Super]
|| (wfromtp eq NoPrefix)
then NoMatchingImplicitsFailure
if !ctx.mode.is(Mode.ImplicitsEnabled) || from.isInstanceOf[Super] then
NoMatchingImplicitsFailure
else {
def adjust(to: Type) = to.stripTypeVar.widenExpr match {
case SelectionProto(name, memberProto, compat, true) =>
Expand Down Expand Up @@ -1129,18 +1126,36 @@ trait Implicits:

val isNotGiven: Boolean = wildProto.classSymbol == defn.NotGivenClass

private def searchTooLarge(): Boolean = ctx.searchHistory match
case root: SearchRoot =>
root.nestedSearches = 1
false
case h =>
val limit = ctx.settings.XimplicitSearchLimit.value
val nestedSearches = h.root.nestedSearches
val result = nestedSearches > limit
if result then
var c = ctx
while c.outer.typer eq ctx.typer do c = c.outer
report.warning(ImplicitSearchTooLargeWarning(limit, h.openSearchPairs), ctx.source.atSpan(span))(using c)
else
h.root.nestedSearches = nestedSearches + 1
result

/** Try to type-check implicit reference, after checking that this is not
* a diverging search
*/
def tryImplicit(cand: Candidate, contextual: Boolean): SearchResult =
if checkDivergence(cand) then
SearchFailure(new DivergingImplicit(cand.ref, wideProto, argument), span)
else {
else if searchTooLarge() then
ImplicitSearchTooLargeFailure
else
val history = ctx.searchHistory.nest(cand, pt)
val typingCtx =
nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
val result = typedImplicit(cand, pt, argument, span)(using typingCtx)
result match {
result match
case res: SearchSuccess =>
ctx.searchHistory.defineBynameImplicit(wideProto, res)
case _ =>
Expand All @@ -1152,8 +1167,6 @@ trait Implicits:
// tests/neg/implicitSearch.check
typingCtx.typerState.gc()
result
}
}

/** Search a list of eligible implicit references */
private def searchImplicit(eligible: List[Candidate], contextual: Boolean): SearchResult =
Expand Down Expand Up @@ -1242,7 +1255,9 @@ trait Implicits:

negateIfNot(tryImplicit(cand, contextual)) match {
case fail: SearchFailure =>
if (fail.isAmbiguous)
if fail eq ImplicitSearchTooLargeFailure then
fail
else if (fail.isAmbiguous)
if migrateTo3 then
val result = rank(remaining, found, NoMatchingImplicitsFailure :: rfailures)
if (result.isSuccess)
Expand Down Expand Up @@ -1411,27 +1426,43 @@ trait Implicits:
rank(sort(eligible), NoMatchingImplicitsFailure, Nil)
end searchImplicit

def isUnderSpecifiedArgument(tp: Type): Boolean =
tp.isRef(defn.NothingClass) || tp.isRef(defn.NullClass) || (tp eq NoPrefix)

private def isUnderspecified(tp: Type): Boolean = tp.stripTypeVar match
case tp: WildcardType =>
!tp.optBounds.exists || isUnderspecified(tp.optBounds.hiBound)
case tp: ViewProto =>
isUnderspecified(tp.resType)
|| tp.resType.isRef(defn.UnitClass)
|| isUnderSpecifiedArgument(tp.argType.widen)
case _ =>
tp.isAny || tp.isAnyRef

private def searchImplicit(contextual: Boolean): SearchResult =
val eligible =
if contextual then ctx.implicits.eligible(wildProto)
else implicitScope(wildProto).eligible
searchImplicit(eligible, contextual) match
case result: SearchSuccess =>
result
case failure: SearchFailure =>
failure.reason match
case _: AmbiguousImplicits => failure
case reason =>
if contextual then
searchImplicit(contextual = false).recoverWith {
failure2 => failure2.reason match
case _: AmbiguousImplicits => failure2
case _ =>
reason match
case (_: DivergingImplicit) => failure
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
}
else failure
if isUnderspecified(wildProto) then
NoMatchingImplicitsFailure
else
val eligible =
if contextual then ctx.implicits.eligible(wildProto)
else implicitScope(wildProto).eligible
searchImplicit(eligible, contextual) match
case result: SearchSuccess =>
result
case failure: SearchFailure =>
failure.reason match
case _: AmbiguousImplicits => failure
case reason =>
if contextual then
searchImplicit(contextual = false).recoverWith {
failure2 => failure2.reason match
case _: AmbiguousImplicits => failure2
case _ =>
reason match
case (_: DivergingImplicit) => failure
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
}
else failure
end searchImplicit

/** Find a unique best implicit reference */
Expand Down Expand Up @@ -1610,13 +1641,17 @@ case class OpenSearch(cand: Candidate, pt: Type, outer: SearchHistory)(using Con
end OpenSearch

/**
* The the state corresponding to the outermost context of an implicit searcch.
* The state corresponding to the outermost context of an implicit searcch.
*/
final class SearchRoot extends SearchHistory:
val root = this
val byname = false
def openSearchPairs = Nil

/** How many expressions were constructed so far in the current toplevel implicit search?
*/
var nestedSearches: Int = 0

/** The dictionary of recursive implicit types and corresponding terms for this search. */
var myImplicitDictionary: mutable.Map[Type, (TermRef, tpd.Tree)] = null
private def implicitDictionary =
Expand Down
1 change: 1 addition & 0 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Expand Up @@ -183,6 +183,7 @@ class CompilationTests {
compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")),
compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")),
).checkExpectedErrors()
}

Expand Down
26 changes: 26 additions & 0 deletions tests/neg-custom-args/i13838.check
@@ -0,0 +1,26 @@
-- Error: tests/neg-custom-args/i13838.scala:10:5 ----------------------------------------------------------------------
10 | foo // error
| ^
|no implicit argument of type Order[X] was found for parameter x$1 of method foo in object FooT
|
|where: X is a type variable
|.
|I found:
|
| FooT.OrderFFooA[F, A](FooT.OrderFFooA[F, A](/* missing */summon[Order[F[Foo[A]]]]))
|
|But given instance OrderFFooA in object FooT produces a diverging implicit search when trying to match type Order[F[Foo[A]]].
-- [E168] Type Warning: tests/neg-custom-args/i13838.scala:10:5 --------------------------------------------------------
10 | foo // error
| ^
| Implicit search problem too large.
| an implicit search was terminated with failure after trying 1000 expressions.
| The root candidate for the search was:
|
| given instance OrderFFooA in object FooT for Order[Any]}
|
| You can change the behavior by setting the `-Ximplicit-search-limit` value.
| Smaller values cause the search to fail faster.
| Larger values might make a very large search problem succeed.

longer explanation available when compiling with `-explain`

0 comments on commit aa25df2

Please sign in to comment.