Skip to content

Commit aa25df2

Browse files
authored
Merge pull request #13886 from dotty-staging/fix-13838
Impose implicit search limit
2 parents 5ce25fe + 739dec3 commit aa25df2

File tree

13 files changed

+232
-43
lines changed

13 files changed

+232
-43
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ private sealed trait XSettings:
218218
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"))
219219
val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))
220220
val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d <jar>)", "")
221+
val XimplicitSearchLimit: Setting[Int] = IntSetting("-Ximplicit-search-limit", "Maximal number of expressions to be generated in an implicit search", 50000)
221222

222223
val XmixinForceForwarders = ChoiceSetting(
223224
name = "-Xmixin-force-forwarders",

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID]:
175175
OverrideErrorID,
176176
MatchableWarningID,
177177
CannotExtendFunctionID,
178-
LossyWideningConstantConversionID
178+
LossyWideningConstantConversionID,
179+
ImplicitSearchTooLargeID
179180

180181
def errorNumber = ordinal - 2
181182

compiler/src/dotty/tools/dotc/reporting/Message.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
124124
def explain = self.explain ++ suffix
125125
override def canExplain = true
126126

127+
/** Override with `true` for messages that should always be shown even if their
128+
* position overlaps another messsage of a different class. On the other hand
129+
* multiple messages of the same class with overlapping positions will lead
130+
* to only a single message of that class to be issued.
131+
*/
132+
def showAlways = false
133+
127134
override def toString = msg
128135
}
129136

compiler/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,24 @@ import core.Contexts._
1010
* are suppressed, unless they are of increasing severity. */
1111
trait UniqueMessagePositions extends Reporter {
1212

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

1515
/** Logs a position and returns true if it was already logged.
1616
* @note Two positions are considered identical for logging if they have the same point.
1717
*/
1818
override def isHidden(dia: Diagnostic)(using Context): Boolean =
19+
extension (dia1: Diagnostic) def hides(dia2: Diagnostic): Boolean =
20+
if dia2.msg.showAlways then dia1.msg.getClass == dia2.msg.getClass
21+
else dia1.level >= dia2.level
1922
super.isHidden(dia) || {
20-
dia.pos.exists && !ctx.settings.YshowSuppressedErrors.value && {
23+
dia.pos.exists
24+
&& !ctx.settings.YshowSuppressedErrors.value
25+
&& {
2126
var shouldHide = false
2227
for (pos <- dia.pos.start to dia.pos.end)
2328
positions get (ctx.source, pos) match {
24-
case Some(level) if level >= dia.level => shouldHide = true
25-
case _ => positions((ctx.source, pos)) = dia.level
29+
case Some(dia1) if dia1.hides(dia) => shouldHide = true
30+
case _ => positions((ctx.source, pos)) = dia
2631
}
2732
shouldHide
2833
}

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import ast.Trees
1818
import config.{Feature, ScalaVersion}
1919
import typer.ErrorReporting.{err, matchReductionAddendum}
2020
import typer.ProtoTypes.ViewProto
21+
import typer.Implicits.Candidate
2122
import scala.util.control.NonFatal
2223
import StdNames.nme
2324
import printing.Formatting.hl
@@ -2515,3 +2516,26 @@ import transform.SymUtils._
25152516
|Inlining such definition would multiply this footprint for each call site.
25162517
|""".stripMargin
25172518
}
2519+
2520+
class ImplicitSearchTooLargeWarning(limit: Int, openSearchPairs: List[(Candidate, Type)])(using Context)
2521+
extends TypeMsg(ImplicitSearchTooLargeID):
2522+
override def showAlways = true
2523+
def showQuery(query: (Candidate, Type)): String =
2524+
i" ${query._1.ref.symbol.showLocated} for ${query._2}}"
2525+
def msg =
2526+
em"""Implicit search problem too large.
2527+
|an implicit search was terminated with failure after trying $limit expressions.
2528+
|The root candidate for the search was:
2529+
|
2530+
|${showQuery(openSearchPairs.last)}
2531+
|
2532+
|You can change the behavior by setting the `-Ximplicit-search-limit` value.
2533+
|Smaller values cause the search to fail faster.
2534+
|Larger values might make a very large search problem succeed.
2535+
|"""
2536+
def explain =
2537+
em"""The overflow happened with the following lists of tried expressions and target types,
2538+
|starting with the root query:
2539+
|
2540+
|${openSearchPairs.reverse.map(showQuery)}%\n%
2541+
"""

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

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,11 @@ object Implicits:
489489
@sharable val NoMatchingImplicitsFailure: SearchFailure =
490490
SearchFailure(NoMatchingImplicits, NoSpan)(using NoContext)
491491

492+
@sharable object ImplicitSearchTooLarge extends NoMatchingImplicits(NoType, EmptyTree, OrderingConstraint.empty)
493+
494+
@sharable val ImplicitSearchTooLargeFailure: SearchFailure =
495+
SearchFailure(ImplicitSearchTooLarge, NoSpan)(using NoContext)
496+
492497
/** An ambiguous implicits failure */
493498
class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree) extends SearchFailureType {
494499
def explanation(using Context): String =
@@ -790,16 +795,8 @@ trait Implicits:
790795
*/
791796
def inferView(from: Tree, to: Type)(using Context): SearchResult = {
792797
record("inferView")
793-
val wfromtp = from.tpe.widen
794-
if to.isAny
795-
|| to.isAnyRef
796-
|| to.isRef(defn.UnitClass)
797-
|| wfromtp.isRef(defn.NothingClass)
798-
|| wfromtp.isRef(defn.NullClass)
799-
|| !ctx.mode.is(Mode.ImplicitsEnabled)
800-
|| from.isInstanceOf[Super]
801-
|| (wfromtp eq NoPrefix)
802-
then NoMatchingImplicitsFailure
798+
if !ctx.mode.is(Mode.ImplicitsEnabled) || from.isInstanceOf[Super] then
799+
NoMatchingImplicitsFailure
803800
else {
804801
def adjust(to: Type) = to.stripTypeVar.widenExpr match {
805802
case SelectionProto(name, memberProto, compat, true) =>
@@ -1129,18 +1126,36 @@ trait Implicits:
11291126

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

1129+
private def searchTooLarge(): Boolean = ctx.searchHistory match
1130+
case root: SearchRoot =>
1131+
root.nestedSearches = 1
1132+
false
1133+
case h =>
1134+
val limit = ctx.settings.XimplicitSearchLimit.value
1135+
val nestedSearches = h.root.nestedSearches
1136+
val result = nestedSearches > limit
1137+
if result then
1138+
var c = ctx
1139+
while c.outer.typer eq ctx.typer do c = c.outer
1140+
report.warning(ImplicitSearchTooLargeWarning(limit, h.openSearchPairs), ctx.source.atSpan(span))(using c)
1141+
else
1142+
h.root.nestedSearches = nestedSearches + 1
1143+
result
1144+
11321145
/** Try to type-check implicit reference, after checking that this is not
11331146
* a diverging search
11341147
*/
11351148
def tryImplicit(cand: Candidate, contextual: Boolean): SearchResult =
11361149
if checkDivergence(cand) then
11371150
SearchFailure(new DivergingImplicit(cand.ref, wideProto, argument), span)
1138-
else {
1151+
else if searchTooLarge() then
1152+
ImplicitSearchTooLargeFailure
1153+
else
11391154
val history = ctx.searchHistory.nest(cand, pt)
11401155
val typingCtx =
11411156
nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
11421157
val result = typedImplicit(cand, pt, argument, span)(using typingCtx)
1143-
result match {
1158+
result match
11441159
case res: SearchSuccess =>
11451160
ctx.searchHistory.defineBynameImplicit(wideProto, res)
11461161
case _ =>
@@ -1152,8 +1167,6 @@ trait Implicits:
11521167
// tests/neg/implicitSearch.check
11531168
typingCtx.typerState.gc()
11541169
result
1155-
}
1156-
}
11571170

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

12431256
negateIfNot(tryImplicit(cand, contextual)) match {
12441257
case fail: SearchFailure =>
1245-
if (fail.isAmbiguous)
1258+
if fail eq ImplicitSearchTooLargeFailure then
1259+
fail
1260+
else if (fail.isAmbiguous)
12461261
if migrateTo3 then
12471262
val result = rank(remaining, found, NoMatchingImplicitsFailure :: rfailures)
12481263
if (result.isSuccess)
@@ -1411,27 +1426,43 @@ trait Implicits:
14111426
rank(sort(eligible), NoMatchingImplicitsFailure, Nil)
14121427
end searchImplicit
14131428

1429+
def isUnderSpecifiedArgument(tp: Type): Boolean =
1430+
tp.isRef(defn.NothingClass) || tp.isRef(defn.NullClass) || (tp eq NoPrefix)
1431+
1432+
private def isUnderspecified(tp: Type): Boolean = tp.stripTypeVar match
1433+
case tp: WildcardType =>
1434+
!tp.optBounds.exists || isUnderspecified(tp.optBounds.hiBound)
1435+
case tp: ViewProto =>
1436+
isUnderspecified(tp.resType)
1437+
|| tp.resType.isRef(defn.UnitClass)
1438+
|| isUnderSpecifiedArgument(tp.argType.widen)
1439+
case _ =>
1440+
tp.isAny || tp.isAnyRef
1441+
14141442
private def searchImplicit(contextual: Boolean): SearchResult =
1415-
val eligible =
1416-
if contextual then ctx.implicits.eligible(wildProto)
1417-
else implicitScope(wildProto).eligible
1418-
searchImplicit(eligible, contextual) match
1419-
case result: SearchSuccess =>
1420-
result
1421-
case failure: SearchFailure =>
1422-
failure.reason match
1423-
case _: AmbiguousImplicits => failure
1424-
case reason =>
1425-
if contextual then
1426-
searchImplicit(contextual = false).recoverWith {
1427-
failure2 => failure2.reason match
1428-
case _: AmbiguousImplicits => failure2
1429-
case _ =>
1430-
reason match
1431-
case (_: DivergingImplicit) => failure
1432-
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
1433-
}
1434-
else failure
1443+
if isUnderspecified(wildProto) then
1444+
NoMatchingImplicitsFailure
1445+
else
1446+
val eligible =
1447+
if contextual then ctx.implicits.eligible(wildProto)
1448+
else implicitScope(wildProto).eligible
1449+
searchImplicit(eligible, contextual) match
1450+
case result: SearchSuccess =>
1451+
result
1452+
case failure: SearchFailure =>
1453+
failure.reason match
1454+
case _: AmbiguousImplicits => failure
1455+
case reason =>
1456+
if contextual then
1457+
searchImplicit(contextual = false).recoverWith {
1458+
failure2 => failure2.reason match
1459+
case _: AmbiguousImplicits => failure2
1460+
case _ =>
1461+
reason match
1462+
case (_: DivergingImplicit) => failure
1463+
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
1464+
}
1465+
else failure
14351466
end searchImplicit
14361467

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

16121643
/**
1613-
* The the state corresponding to the outermost context of an implicit searcch.
1644+
* The state corresponding to the outermost context of an implicit searcch.
16141645
*/
16151646
final class SearchRoot extends SearchHistory:
16161647
val root = this
16171648
val byname = false
16181649
def openSearchPairs = Nil
16191650

1651+
/** How many expressions were constructed so far in the current toplevel implicit search?
1652+
*/
1653+
var nestedSearches: Int = 0
1654+
16201655
/** The dictionary of recursive implicit types and corresponding terms for this search. */
16211656
var myImplicitDictionary: mutable.Map[Type, (TermRef, tpd.Tree)] = null
16221657
private def implicitDictionary =

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class CompilationTests {
183183
compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
184184
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
185185
compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")),
186+
compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")),
186187
).checkExpectedErrors()
187188
}
188189

tests/neg-custom-args/i13838.check

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-- Error: tests/neg-custom-args/i13838.scala:10:5 ----------------------------------------------------------------------
2+
10 | foo // error
3+
| ^
4+
|no implicit argument of type Order[X] was found for parameter x$1 of method foo in object FooT
5+
|
6+
|where: X is a type variable
7+
|.
8+
|I found:
9+
|
10+
| FooT.OrderFFooA[F, A](FooT.OrderFFooA[F, A](/* missing */summon[Order[F[Foo[A]]]]))
11+
|
12+
|But given instance OrderFFooA in object FooT produces a diverging implicit search when trying to match type Order[F[Foo[A]]].
13+
-- [E168] Type Warning: tests/neg-custom-args/i13838.scala:10:5 --------------------------------------------------------
14+
10 | foo // error
15+
| ^
16+
| Implicit search problem too large.
17+
| an implicit search was terminated with failure after trying 1000 expressions.
18+
| The root candidate for the search was:
19+
|
20+
| given instance OrderFFooA in object FooT for Order[Any]}
21+
|
22+
| You can change the behavior by setting the `-Ximplicit-search-limit` value.
23+
| Smaller values cause the search to fail faster.
24+
| Larger values might make a very large search problem succeed.
25+
26+
longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)