Skip to content
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

Impose implicit search limit #13886

Merged
merged 7 commits into from Nov 9, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`