Skip to content

Commit 7263790

Browse files
authored
Backport "Avoid generating given definitions that loop" (#19477)
Backports #19282 with later improvements (#19392, #19411) to 3.4.0
2 parents d4bf6ae + b9857ef commit 7263790

27 files changed

+413
-53
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ enum SourceVersion:
1010
case `3.2-migration`, `3.2`
1111
case `3.3-migration`, `3.3`
1212
case `3.4-migration`, `3.4`
13+
case `3.5-migration`, `3.5`
1314
// !!! Keep in sync with scala.runtime.stdlibPatches.language !!!
1415
case `future-migration`, `future`
1516

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

+119-30
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.*
@@ -93,7 +93,7 @@ object Implicits:
9393
if (initctx eq NoContext) initctx else initctx.retractMode(Mode.ImplicitsEnabled)
9494
protected given Context = irefCtx
9595

96-
/** The nesting level of this context. Non-zero only in ContextialImplicits */
96+
/** The nesting level of this context. Non-zero only in ContextualImplicits */
9797
def level: Int = 0
9898

9999
/** The implicit references */
@@ -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`
@@ -408,6 +408,13 @@ object Implicits:
408408
}
409409
}
410410

411+
/** Search mode to use for possibly avoiding looping givens */
412+
enum SearchMode:
413+
case Old, // up to 3.3, old mode w/o protection
414+
CompareWarn, // from 3.4, old mode, warn if new mode would change result
415+
CompareErr, // from 3.5, old mode, error if new mode would change result
416+
New // from future, new mode where looping givens are avoided
417+
411418
/** The result of an implicit search */
412419
sealed abstract class SearchResult extends Showable {
413420
def tree: Tree
@@ -1550,35 +1557,113 @@ trait Implicits:
15501557
case _ =>
15511558
tp.isAny || tp.isAnyRef
15521559

1553-
private def searchImplicit(contextual: Boolean): SearchResult =
1560+
/** Search implicit in context `ctxImplicits` or else in implicit scope
1561+
* of expected type if `ctxImplicits == null`.
1562+
*/
1563+
private def searchImplicit(ctxImplicits: ContextualImplicits | Null, mode: SearchMode): SearchResult =
15541564
if isUnderspecified(wildProto) then
15551565
SearchFailure(TooUnspecific(pt), span)
15561566
else
1557-
val eligible =
1558-
if contextual then
1567+
val contextual = ctxImplicits != null
1568+
val preEligible = // the eligible candidates, ignoring positions
1569+
if ctxImplicits != null then
15591570
if ctx.gadt.isNarrowing then
15601571
withoutMode(Mode.ImplicitsEnabled) {
1561-
ctx.implicits.uncachedEligible(wildProto)
1572+
ctxImplicits.uncachedEligible(wildProto)
15621573
}
1563-
else ctx.implicits.eligible(wildProto)
1574+
else ctxImplicits.eligible(wildProto)
15641575
else implicitScope(wildProto).eligible
1565-
searchImplicit(eligible, contextual) match
1566-
case result: SearchSuccess =>
1567-
result
1568-
case failure: SearchFailure =>
1569-
failure.reason match
1570-
case _: AmbiguousImplicits => failure
1571-
case reason =>
1572-
if contextual then
1573-
searchImplicit(contextual = false).recoverWith {
1574-
failure2 => failure2.reason match
1575-
case _: AmbiguousImplicits => failure2
1576-
case _ =>
1577-
reason match
1578-
case (_: DivergingImplicit) => failure
1579-
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
1580-
}
1581-
else failure
1576+
1577+
/** Does candidate `cand` come too late for it to be considered as an
1578+
* eligible candidate? This is the case if `cand` appears in the same
1579+
* scope as a given definition of the form `given ... = ...` that
1580+
* encloses the search point and `cand` comes later in the source or
1581+
* coincides with that given definition.
1582+
*/
1583+
def comesTooLate(cand: Candidate): Boolean =
1584+
val candSym = cand.ref.symbol
1585+
def candSucceedsGiven(sym: Symbol): Boolean =
1586+
val owner = sym.owner
1587+
if owner == candSym.owner then
1588+
sym.is(GivenVal) && sym.span.exists && sym.span.start <= candSym.span.start
1589+
else if owner.isClass then false
1590+
else candSucceedsGiven(owner)
1591+
1592+
ctx.isTyper
1593+
&& !candSym.isOneOf(TermParamOrAccessor | Synthetic)
1594+
&& candSym.span.exists
1595+
&& candSucceedsGiven(ctx.owner)
1596+
end comesTooLate
1597+
1598+
val eligible = // the eligible candidates that come before the search point
1599+
if contextual && mode != SearchMode.Old
1600+
then preEligible.filterNot(comesTooLate)
1601+
else preEligible
1602+
1603+
def checkResolutionChange(result: SearchResult) =
1604+
if (eligible ne preEligible) && mode != SearchMode.New then
1605+
searchImplicit(preEligible, contextual) match
1606+
case prevResult: SearchSuccess =>
1607+
def remedy = pt match
1608+
case _: SelectionProto =>
1609+
"conversion,\n - use an import to get extension method into scope"
1610+
case _: ViewProto =>
1611+
"conversion"
1612+
case _ =>
1613+
"argument"
1614+
1615+
def showResult(r: SearchResult) = r match
1616+
case r: SearchSuccess => ctx.printer.toTextRef(r.ref).show
1617+
case r => r.show
1618+
1619+
result match
1620+
case result: SearchSuccess if prevResult.ref frozen_=:= result.ref =>
1621+
// OK
1622+
case _ =>
1623+
val msg =
1624+
em"""Result of implicit search for $pt will change.
1625+
|Current result ${showResult(prevResult)} will be no longer eligible
1626+
| because it is not defined before the search position.
1627+
|Result with new rules: ${showResult(result)}.
1628+
|To opt into the new rules, compile with `-source future` or use
1629+
|the `scala.language.future` language import.
1630+
|
1631+
|To fix the problem without the language import, you could try one of the following:
1632+
| - use a `given ... with` clause as the enclosing given,
1633+
| - rearrange definitions so that ${showResult(prevResult)} comes earlier,
1634+
| - use an explicit $remedy."""
1635+
if mode == SearchMode.CompareErr
1636+
then report.error(msg, srcPos)
1637+
else report.warning(msg.append("\nThis will be an error in Scala 3.5 and later."), srcPos)
1638+
prevResult
1639+
case prevResult: SearchFailure => result
1640+
else result
1641+
end checkResolutionChange
1642+
1643+
checkResolutionChange:
1644+
searchImplicit(eligible, contextual).recoverWith:
1645+
case failure: SearchFailure =>
1646+
failure.reason match
1647+
case _: AmbiguousImplicits => failure
1648+
case reason =>
1649+
if contextual then
1650+
// If we filtered out some candidates for being too late, we should
1651+
// do another contextual search further out, since the dropped candidates
1652+
// might have shadowed an eligible candidate in an outer level.
1653+
// Otherwise, proceed with a search of the implicit scope.
1654+
val newCtxImplicits =
1655+
if eligible eq preEligible then null
1656+
else ctxImplicits.nn.outerImplicits: ContextualImplicits | Null
1657+
// !!! Dotty problem: without the ContextualImplicits | Null type ascription
1658+
// we get a Ycheck failure after arrayConstructors due to "Types differ"
1659+
searchImplicit(newCtxImplicits, SearchMode.New).recoverWith:
1660+
failure2 => failure2.reason match
1661+
case _: AmbiguousImplicits => failure2
1662+
case _ =>
1663+
reason match
1664+
case (_: DivergingImplicit) => failure
1665+
case _ => List(failure, failure2).maxBy(_.tree.treeSize)
1666+
else failure
15821667
end searchImplicit
15831668

15841669
/** Find a unique best implicit reference */
@@ -1595,7 +1680,11 @@ trait Implicits:
15951680
case ref: TermRef =>
15961681
SearchSuccess(tpd.ref(ref).withSpan(span.startPos), ref, 0)(ctx.typerState, ctx.gadt)
15971682
case _ =>
1598-
searchImplicit(contextual = true)
1683+
searchImplicit(ctx.implicits,
1684+
if sourceVersion.isAtLeast(SourceVersion.future) then SearchMode.New
1685+
else if sourceVersion.isAtLeast(SourceVersion.`3.5`) then SearchMode.CompareErr
1686+
else if sourceVersion.isAtLeast(SourceVersion.`3.4`) then SearchMode.CompareWarn
1687+
else SearchMode.Old)
15991688
end bestImplicit
16001689

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

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,36 @@ 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.** The following change is currently enabled in `-source future`:
167169

170+
Implicit resolution now avoids generating recursive givens that can lead to an infinite loop at runtime. Here is an example:
168171

172+
```scala
173+
object Prices {
174+
opaque type Price = BigDecimal
175+
176+
object Price{
177+
given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
178+
}
179+
}
180+
```
181+
182+
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:
183+
184+
- When doing an implicit search while checking the implementation of a `given` definition `G` of the form
185+
```
186+
given ... = ....
187+
```
188+
discard all search results that lead back to `G` or to a given with the same owner as `G` that comes later in the source than `G`.
189+
190+
The new behavior is currently enabled in `source.future` and will be enabled at the earliest in Scala 3.6. For earlier source versions, the behavior is as
191+
follows:
192+
193+
- Scala 3.3: no change
194+
- Scala 3.4: A warning is issued where the behavior will change in 3.future.
195+
- Scala 3.5: An error is issued where the behavior will change in 3.future.
196+
197+
Old-style implicit definitions are unaffected by this change.
169198
170-
[//]: # todo: expand with precise rules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
layout: doc-page
3+
title: Given Loop Prevention
4+
redirectFrom: /docs/reference/other-new-features/into-modifier.html
5+
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/into-modifier.html
6+
---
7+
8+
Implicit resolution now avoids generating recursive givens that can lead to an infinite loop at runtime. Here is an example:
9+
10+
```scala
11+
object Prices {
12+
opaque type Price = BigDecimal
13+
14+
object Price{
15+
given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
16+
}
17+
}
18+
```
19+
20+
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:
21+
22+
- When doing an implicit search while checking the implementation of a `given` definition `G` of the form
23+
```
24+
given ... = ....
25+
```
26+
discard all search results that lead back to `G` or to a given with the same owner as `G` that comes later in the source than `G`.
27+
28+
The new behavior is enabled with the `experimental.givenLoopPrevention` language import. If no such import or setting is given, a warning is issued where the behavior would change under that import (for source version 3.4 and later).
29+
30+
Old-style implicit definitions are unaffected by this change.
31+

tests/neg/i15474.check

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- Error: tests/neg/i15474.scala:6:39 ----------------------------------------------------------------------------------
2+
6 | given c: Conversion[ String, Int ] = _.toInt // error
3+
| ^
4+
| Result of implicit search for ?{ toInt: ? } will change.
5+
| Current result Test2.c will be no longer eligible
6+
| because it is not defined before the search position.
7+
| Result with new rules: augmentString.
8+
| To opt into the new rules, compile with `-source future` or use
9+
| the `scala.language.future` language import.
10+
|
11+
| To fix the problem without the language import, you could try one of the following:
12+
| - use a `given ... with` clause as the enclosing given,
13+
| - rearrange definitions so that Test2.c comes earlier,
14+
| - use an explicit conversion,
15+
| - use an import to get extension method into scope.
16+
| This will be an error in Scala 3.5 and later.
17+
-- Error: tests/neg/i15474.scala:12:56 ---------------------------------------------------------------------------------
18+
12 | given Ordering[Price] = summon[Ordering[BigDecimal]] // error
19+
| ^
20+
| Result of implicit search for Ordering[BigDecimal] will change.
21+
| Current result Prices.Price.given_Ordering_Price will be no longer eligible
22+
| because it is not defined before the search position.
23+
| Result with new rules: scala.math.Ordering.BigDecimal.
24+
| To opt into the new rules, compile with `-source future` or use
25+
| the `scala.language.future` language import.
26+
|
27+
| To fix the problem without the language import, you could try one of the following:
28+
| - use a `given ... with` clause as the enclosing given,
29+
| - rearrange definitions so that Prices.Price.given_Ordering_Price comes earlier,
30+
| - use an explicit argument.
31+
| This will be an error in Scala 3.5 and later.

tests/neg/i15474.scala

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
import scala.language.implicitConversions
44

5-
object Test1:
6-
given c: Conversion[ String, Int ] with
7-
def apply(from: String): Int = from.toInt // error
8-
95
object Test2:
10-
given c: Conversion[ String, Int ] = _.toInt // loop not detected, could be used as a fallback to avoid the warning.
6+
given c: Conversion[ String, Int ] = _.toInt // error
117

128
object Prices {
139
opaque type Price = BigDecimal
1410

1511
object Price{
1612
given Ordering[Price] = summon[Ordering[BigDecimal]] // error
1713
}
18-
}
14+
}
15+
16+

tests/neg/i15474b.check

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Error: tests/neg/i15474b.scala:7:40 ---------------------------------------------------------------------------------
2+
7 | def apply(from: String): Int = from.toInt // error: infinite loop in function body
3+
| ^^^^^^^^^^
4+
| Infinite loop in function body
5+
| Test1.c.apply(from).toInt

tests/neg/i15474b.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//> using options -Xfatal-warnings
2+
3+
import scala.language.implicitConversions
4+
5+
object Test1:
6+
given c: Conversion[ String, Int ] with
7+
def apply(from: String): Int = from.toInt // error: infinite loop in function body
8+

tests/neg/i6716.check

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- Error: tests/neg/i6716.scala:12:39 ----------------------------------------------------------------------------------
2+
12 | given Monad[Bar] = summon[Monad[Foo]] // error
3+
| ^
4+
| Result of implicit search for Monad[Foo] will change.
5+
| Current result Bar.given_Monad_Bar will be no longer eligible
6+
| because it is not defined before the search position.
7+
| Result with new rules: Foo.given_Monad_Foo.
8+
| To opt into the new rules, compile with `-source future` or use
9+
| the `scala.language.future` language import.
10+
|
11+
| To fix the problem without the language import, you could try one of the following:
12+
| - use a `given ... with` clause as the enclosing given,
13+
| - rearrange definitions so that Bar.given_Monad_Bar comes earlier,
14+
| - use an explicit argument.
15+
| This will be an error in Scala 3.5 and later.

0 commit comments

Comments
 (0)