Skip to content

Commit bc36acb

Browse files
committed
Avoid generating given definitions that loop
1 parent ec2b8bc commit bc36acb

File tree

12 files changed

+161
-30
lines changed

12 files changed

+161
-30
lines changed

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

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import Scopes.newScope
2626
import Typer.BindingPrec, BindingPrec.*
2727
import Hashable.*
2828
import util.{EqHashMap, Stats}
29-
import config.{Config, Feature}
30-
import Feature.migrateTo3
29+
import config.{Config, Feature, SourceVersion}
30+
import Feature.{migrateTo3, sourceVersion}
3131
import config.Printers.{implicits, implicitsDetailed}
3232
import collection.mutable
3333
import reporting.*
@@ -324,7 +324,7 @@ object Implicits:
324324
/** Is this the outermost implicits? This is the case if it either the implicits
325325
* of NoContext, or the last one before it.
326326
*/
327-
private def isOuterMost = {
327+
private def isOutermost = {
328328
val finalImplicits = NoContext.implicits
329329
(this eq finalImplicits) || (outerImplicits eqn finalImplicits)
330330
}
@@ -356,7 +356,7 @@ object Implicits:
356356
Stats.record("uncached eligible")
357357
if monitored then record(s"check uncached eligible refs in irefCtx", refs.length)
358358
val ownEligible = filterMatching(tp)
359-
if isOuterMost then ownEligible
359+
if isOutermost then ownEligible
360360
else combineEligibles(ownEligible, outerImplicits.nn.uncachedEligible(tp))
361361

362362
/** The implicit references that are eligible for type `tp`. */
@@ -383,7 +383,7 @@ object Implicits:
383383
private def computeEligible(tp: Type): List[Candidate] = /*>|>*/ trace(i"computeEligible $tp in $refs%, %", implicitsDetailed) /*<|<*/ {
384384
if (monitored) record(s"check eligible refs in irefCtx", refs.length)
385385
val ownEligible = filterMatching(tp)
386-
if isOuterMost then ownEligible
386+
if isOutermost then ownEligible
387387
else combineEligibles(ownEligible, outerImplicits.nn.eligible(tp))
388388
}
389389

@@ -392,7 +392,7 @@ object Implicits:
392392

393393
override def toString: String = {
394394
val own = i"(implicits: $refs%, %)"
395-
if (isOuterMost) own else own + "\n " + outerImplicits
395+
if (isOutermost) own else own + "\n " + outerImplicits
396396
}
397397

398398
/** This context, or a copy, ensuring root import from symbol `root`
@@ -1550,18 +1550,59 @@ trait Implicits:
15501550
case _ =>
15511551
tp.isAny || tp.isAnyRef
15521552

1553-
private def searchImplicit(contextual: Boolean): SearchResult =
1553+
/** Search implicit in context `ctxImplicits` or else in implicit scope
1554+
* of expected type if `ctxImplicits == null`.
1555+
*/
1556+
private def searchImplicit(ctxImplicits: ContextualImplicits | Null): SearchResult =
15541557
if isUnderspecified(wildProto) then
15551558
SearchFailure(TooUnspecific(pt), span)
15561559
else
1557-
val eligible =
1560+
val contextual = ctxImplicits != null
1561+
val preEligible = // the eligible candidates, ignoring positions
15581562
if contextual then
15591563
if ctx.gadt.isNarrowing then
15601564
withoutMode(Mode.ImplicitsEnabled) {
15611565
ctx.implicits.uncachedEligible(wildProto)
15621566
}
15631567
else ctx.implicits.eligible(wildProto)
15641568
else implicitScope(wildProto).eligible
1569+
1570+
/** Does candidate `cand` come too late for it to be considered as an
1571+
* eligible candidate? This is the case if `cand` appears in the same
1572+
* scope as a given definition enclosing the search point and comes
1573+
* later in the source or coincides with that given definition.
1574+
*/
1575+
def comesTooLate(cand: Candidate): Boolean =
1576+
val candSym = cand.ref.symbol
1577+
def candSucceedsGiven(sym: Symbol): Boolean =
1578+
if sym.owner == candSym.owner then
1579+
if sym.is(ModuleClass) then candSucceedsGiven(sym.sourceModule)
1580+
else sym.is(Given) && sym.span.exists && sym.span.start <= candSym.span.start
1581+
else if sym.is(Package) then false
1582+
else candSucceedsGiven(sym.owner)
1583+
1584+
ctx.isTyper
1585+
&& !candSym.isOneOf(TermParamOrAccessor | Synthetic)
1586+
&& candSym.span.exists
1587+
&& candSucceedsGiven(ctx.owner)
1588+
end comesTooLate
1589+
1590+
val eligible = if contextual then preEligible.filterNot(comesTooLate) else preEligible
1591+
1592+
def checkResolutionChange(result: SearchResult) = result match
1593+
case result: SearchSuccess
1594+
if (eligible ne preEligible) && !sourceVersion.isAtLeast(SourceVersion.`future`) =>
1595+
searchImplicit(preEligible.diff(eligible), contextual) match
1596+
case prevResult: SearchSuccess =>
1597+
report.error(
1598+
em"""Warning: result of implicit search for $pt will change.
1599+
|current result: ${prevResult.ref.symbol.showLocated}
1600+
|result with -source future: ${result.ref.symbol.showLocated}""",
1601+
srcPos
1602+
)
1603+
case _ =>
1604+
case _ =>
1605+
15651606
searchImplicit(eligible, contextual) match
15661607
case result: SearchSuccess =>
15671608
result
@@ -1570,14 +1611,24 @@ trait Implicits:
15701611
case _: AmbiguousImplicits => failure
15711612
case reason =>
15721613
if contextual then
1573-
searchImplicit(contextual = false).recoverWith {
1614+
// If we filtered out some candidates for being too late, we should
1615+
// do another contextual search further out, since the dropped candidates
1616+
// might have shadowed an eligible candidate in an outer level.
1617+
// Otherwise, proceed with a search of the implicit scope.
1618+
val newCtxImplicits =
1619+
if eligible eq preEligible then null
1620+
else ctxImplicits.nn.outerImplicits: ContextualImplicits | Null
1621+
// !!! Dotty problem: without the ContextualImplicits | Null type ascription
1622+
// we get a Ycheck failure after arrayConstructors due to "Types differ"
1623+
val result = searchImplicit(newCtxImplicits).recoverWith:
15741624
failure2 => failure2.reason match
15751625
case _: AmbiguousImplicits => failure2
15761626
case _ =>
15771627
reason match
15781628
case (_: DivergingImplicit) => failure
15791629
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
1580-
}
1630+
checkResolutionChange(result)
1631+
result
15811632
else failure
15821633
end searchImplicit
15831634

@@ -1595,7 +1646,7 @@ trait Implicits:
15951646
case ref: TermRef =>
15961647
SearchSuccess(tpd.ref(ref).withSpan(span.startPos), ref, 0)(ctx.typerState, ctx.gadt)
15971648
case _ =>
1598-
searchImplicit(contextual = true)
1649+
searchImplicit(ctx.implicits)
15991650
end bestImplicit
16001651

16011652
def implicitScope(tp: Type): OfTypeImplicits = ctx.run.nn.implicitScope(tp)

docs/_docs/reference/changed-features/implicit-resolution.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,27 @@ The new rules are as follows: An implicit `a` defined in `A` is more specific th
163163

164164
Condition (*) is new. It is necessary to ensure that the defined relation is transitive.
165165

166+
[//]: # todo: expand with precise rules
166167

168+
**9.** Implicit resolution now tries to avoid recursive givens that can lead to an infinite loop at runtime. Here is an example:
167169

170+
```scala
171+
object Prices {
172+
opaque type Price = BigDecimal
168173

174+
object Price{
175+
given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
176+
}
177+
}
178+
```
179+
180+
Previously, implicit resolution would resolve the `summon` to the given in `Price`, leading to an infinite loop (a warning was issued in that case). We now use the underlying given in `BigDecimal` instead. We achieve that by adding the following rule for implicit search:
181+
182+
- When doing an implicit search while checking the implementation of a `given` definition `G`, discard all search results that lead back to `G` or to a given
183+
with the same owner as `G` that comes later in the source than `G`.
184+
185+
The new behavior is enabled under `-source future`. In earlier versions, a
186+
warning is issued where that behavior will change.
187+
188+
Old-style implicit definitions are unaffected by this change.
169189

170-
[//]: # todo: expand with precise rules

tests/neg/i15474.check

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Error: tests/neg/i15474.scala:16:56 ---------------------------------------------------------------------------------
2+
16 | given Ordering[Price] = summon[Ordering[BigDecimal]] // error
3+
| ^
4+
| Warning: result of implicit search for Ordering[BigDecimal] will change.
5+
| current result: given instance given_Ordering_Price in object Price
6+
| result with -source future: object BigDecimal in object Ordering

tests/neg/i15474.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import scala.language.implicitConversions
44

55
object Test1:
66
given c: Conversion[ String, Int ] with
7-
def apply(from: String): Int = from.toInt // error
7+
def apply(from: String): Int = from.toInt // was error, now avoided
88

99
object Test2:
10-
given c: Conversion[ String, Int ] = _.toInt // loop not detected, could be used as a fallback to avoid the warning.
10+
given c: Conversion[ String, Int ] = _.toInt // now avoided, was loop not detected, could be used as a fallback to avoid the warning.
1111

1212
object Prices {
1313
opaque type Price = BigDecimal
1414

1515
object Price{
1616
given Ordering[Price] = summon[Ordering[BigDecimal]] // error
1717
}
18-
}
18+
}
19+
20+

tests/neg/i6716.check

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Error: tests/neg/i6716.scala:12:39 ----------------------------------------------------------------------------------
2+
12 | given Monad[Bar] = summon[Monad[Foo]] // error
3+
| ^
4+
| Warning: result of implicit search for Monad[Foo] will change.
5+
| current result: given instance given_Monad_Bar in object Bar
6+
| result with -source future: object given_Monad_Foo in object Foo

tests/neg/i6716.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//> using options -Xfatal-warnings
2+
3+
trait Monad[T]:
4+
def id: String
5+
class Foo
6+
object Foo {
7+
given Monad[Foo] with { def id = "Foo" }
8+
}
9+
10+
opaque type Bar = Foo
11+
object Bar {
12+
given Monad[Bar] = summon[Monad[Foo]] // error
13+
}
14+
15+
object Test extends App {
16+
println(summon[Monad[Foo]].id)
17+
println(summon[Monad[Bar]].id)
18+
}

tests/pos/i15474.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//> using options -Xfatal-warnings
2+
import scala.language.implicitConversions
3+
import language.future
4+
5+
object Test1:
6+
given c: Conversion[ String, Int ] with
7+
def apply(from: String): Int = from.toInt // was error, now avoided
8+
9+
object Test2:
10+
given c: Conversion[ String, Int ] = _.toInt // now avoided, was loop not detected, could be used as a fallback to avoid the warning.
11+
12+
object Prices {
13+
opaque type Price = BigDecimal
14+
15+
object Price{
16+
given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
17+
}
18+
}
19+
20+

tests/pos/i6716.scala

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/run/i17115.check

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
4
2+
5
File renamed without changes.

tests/run/i6716.check

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Foo
2+
Foo

tests/run/i6716.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//> using options -Xfatal-warnings
2+
3+
import language.future
4+
5+
trait Monad[T]:
6+
def id: String
7+
class Foo
8+
object Foo {
9+
given Monad[Foo] with { def id = "Foo" }
10+
}
11+
12+
opaque type Bar = Foo
13+
object Bar {
14+
given Monad[Bar] = summon[Monad[Foo]] // error
15+
}
16+
17+
object Test extends App {
18+
println(summon[Monad[Foo]].id)
19+
println(summon[Monad[Bar]].id)
20+
}

0 commit comments

Comments
 (0)