Skip to content

Commit de97131

Browse files
committed
Change rules for given prioritization
Consider the following program: ```scala class A class B extends A class C extends A given A = A() given B = B() given C = C() def f(using a: A, b: B, c: C) = println(a.getClass) println(b.getClass) println(c.getClass) @main def Test = f ``` With the current rules, this would fail with an ambiguity error between B and C when trying to synthesize the A parameter. This is a problem without an easy remedy. We can fix this problem by flipping the priority for implicit arguments. Instead of requiring an argument to be most _specific_, we now require it to be most _general_ while still conforming to the formal parameter. There are three justifications for this change, which at first glance seems quite drastic: - It gives us a natural way to deal with inheritance triangles like the one in the code above. Such triangles are quite common. - Intuitively, we want to get the closest possible match between required formal parameter type and synthetisized argument. The "most general" rule provides that. - We already do a crucial part of this. Namely, with current rules we interpolate all type variables in an implicit argument downwards, no matter what their variance is. This makes no sense in theory, but solves hairy problems with contravariant typeclasses like `Comparable`. Instead of this hack, we now do something more principled, by flipping the direction everywhere, preferring general over specific, instead of just flipping contravariant type parameters. The behavior is dependent on the Scala version - Old behavior: up to 3.4 - New behavior: from 3.5, 3.5-migration warns on behavior change The CB builds under the new rules. One fix was needed for a shapeless 3 deriving test. There was a typo: mkInstances instead of mkProductInstances, which previously got healed by accident because of the most specific rule. Also: Don't flip contravariant type arguments for overloading resolution Flipping contravariant type arguments was needed for implicit search where it will be replaced by a more general scheme. But it makes no sense for overloading resolution. For overloading resolution, we want to pick the most specific alternative, analogous to us picking the most specific instantiation when we force a fully defined type. Also: Disable implicit search everywhere for disambiaguation Previously, one disambiguation step missed that, whereas implicits were turned off everywhere else.
1 parent c7a0459 commit de97131

16 files changed

+255
-78
lines changed

compiler/src/dotty/tools/dotc/core/Mode.scala

+10-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ object Mode {
4141
val Pattern: Mode = newMode(0, "Pattern")
4242
val Type: Mode = newMode(1, "Type")
4343

44+
val PatternOrTypeBits: Mode = Pattern | Type
45+
4446
val ImplicitsEnabled: Mode = newMode(2, "ImplicitsEnabled")
4547
val InferringReturnType: Mode = newMode(3, "InferringReturnType")
4648

@@ -101,16 +103,19 @@ object Mode {
101103
*/
102104
val CheckBoundsOrSelfType: Mode = newMode(14, "CheckBoundsOrSelfType")
103105

104-
/** Use Scala2 scheme for overloading and implicit resolution */
105-
val OldOverloadingResolution: Mode = newMode(15, "OldOverloadingResolution")
106+
/** Use previous Scheme for implicit resolution. Currently significant
107+
* in 3.0-migration where we use Scala-2's scheme instead and in 3.5-migration
108+
* where we use the previous scheme up to 3.4 instead.
109+
*/
110+
val OldImplicitResolution: Mode = newMode(15, "OldImplicitResolution")
106111

107112
/** Treat CapturingTypes as plain AnnotatedTypes even in phase CheckCaptures.
108-
* Reuses the value of OldOverloadingResolution to save Mode bits.
109-
* This is OK since OldOverloadingResolution only affects implicit search, which
113+
* Reuses the value of OldImplicitResolution to save Mode bits.
114+
* This is OK since OldImplicitResolution only affects implicit search, which
110115
* is done during phases Typer and Inlinig, and IgnoreCaptures only has an
111116
* effect during phase CheckCaptures.
112117
*/
113-
val IgnoreCaptures = OldOverloadingResolution
118+
val IgnoreCaptures = OldImplicitResolution
114119

115120
/** Allow hk applications of type lambdas to wildcard arguments;
116121
* used for checking that such applications do not normally arise
@@ -120,8 +125,6 @@ object Mode {
120125
/** Read original positions when unpickling from TASTY */
121126
val ReadPositions: Mode = newMode(17, "ReadPositions")
122127

123-
val PatternOrTypeBits: Mode = Pattern | Type
124-
125128
/** We are elaborating the fully qualified name of a package clause.
126129
* In this case, identifiers should never be imported.
127130
*/

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

+79-52
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ProtoTypes.*
2222
import Inferencing.*
2323
import reporting.*
2424
import Nullables.*, NullOpsDecorator.*
25-
import config.Feature
25+
import config.{Feature, SourceVersion}
2626

2727
import collection.mutable
2828
import config.Printers.{overload, typr, unapp}
@@ -1657,6 +1657,12 @@ trait Applications extends Compatibility {
16571657
/** Compare two alternatives of an overloaded call or an implicit search.
16581658
*
16591659
* @param alt1, alt2 Non-overloaded references indicating the two choices
1660+
* @param preferGeneral When comparing two value types, prefer the more general one
1661+
* over the more specific one iff `preferGeneral` is true.
1662+
* `preferGeneral` is set to `true` when we compare two given values, since
1663+
* then we want the most general evidence that matches the target
1664+
* type. It is set to `false` for overloading resolution, when we want the
1665+
* most specific type instead.
16601666
* @return 1 if 1st alternative is preferred over 2nd
16611667
* -1 if 2nd alternative is preferred over 1st
16621668
* 0 if neither alternative is preferred over the other
@@ -1672,27 +1678,28 @@ trait Applications extends Compatibility {
16721678
* an alternative that takes more implicit parameters wins over one
16731679
* that takes fewer.
16741680
*/
1675-
def compare(alt1: TermRef, alt2: TermRef)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
1681+
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
16761682
record("resolveOverloaded.compare")
16771683

1678-
/** Is alternative `alt1` with type `tp1` as specific as alternative
1684+
val compareGivens = alt1.symbol.is(Given) || alt2.symbol.is(Given)
1685+
1686+
/** Is alternative `alt1` with type `tp1` as good as alternative
16791687
* `alt2` with type `tp2` ?
16801688
*
1681-
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as specific as `alt2`
1689+
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as good as `alt2`
16821690
* if `alt1` is nullary or `alt2` is applicable to arguments (p1, ..., pn) of
16831691
* types T1,...,Tn. If the last parameter `pn` has a vararg type T*, then
16841692
* `alt1` must be applicable to arbitrary numbers of `T` parameters (which
16851693
* implies that it must be a varargs method as well).
16861694
* 2. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as
1687-
* specific as `alt2` of type `tp2` if T is as specific as `tp2` under the
1695+
* good as `alt2` of type `tp2` if T is as good as `tp2` under the
16881696
* assumption that for i = 1,...,n each ai is an abstract type name bounded
16891697
* from below by Li and from above by Ui.
16901698
* 3. A member of any other type `tp1` is:
1691-
* a. always as specific as a method or a polymorphic method.
1692-
* b. as specific as a member of any other type `tp2` if `tp1` is compatible
1693-
* with `tp2`.
1699+
* a. always as good as a method or a polymorphic method.
1700+
* b. as good as a member of any other type `tp2` is `asGoodValueType(tp1, tp2) = true`
16941701
*/
1695-
def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) {
1702+
def isAsGood(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) {
16961703
tp1 match
16971704
case tp1: MethodType => // (1)
16981705
tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType]
@@ -1714,69 +1721,89 @@ trait Applications extends Compatibility {
17141721
fullyDefinedType(tp1Params, "type parameters of alternative", alt1.symbol.srcPos)
17151722

17161723
val tparams = newTypeParams(alt1.symbol, tp1.paramNames, EmptyFlags, tp1.instantiateParamInfos(_))
1717-
isAsSpecific(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
1724+
isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
17181725
}
17191726
case _ => // (3)
17201727
tp2 match
17211728
case tp2: MethodType => true // (3a)
17221729
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
17231730
case tp2: PolyType => // (3b)
1724-
explore(isAsSpecificValueType(tp1, instantiateWithTypeVars(tp2)))
1731+
explore(isAsGoodValueType(tp1, instantiateWithTypeVars(tp2)))
17251732
case _ => // 3b)
1726-
isAsSpecificValueType(tp1, tp2)
1733+
isAsGoodValueType(tp1, tp2)
17271734
}
17281735

1729-
/** Test whether value type `tp1` is as specific as value type `tp2`.
1730-
* Let's abbreviate this to `tp1 <:s tp2`.
1731-
* Previously, `<:s` was the same as `<:`. This behavior is still
1732-
* available under mode `Mode.OldOverloadingResolution`. The new behavior
1733-
* is different, however. Here, `T <:s U` iff
1736+
/** Test whether value type `tp1` is as good as value type `tp2`.
1737+
* Let's abbreviate this to `tp1 <:p tp2`. The behavior depends on the Scala version
1738+
* and mode.
17341739
*
1735-
* flip(T) <: flip(U)
1740+
* - In Scala 2, `<:p` was the same as `<:`. This behavior is still
1741+
* available in 3.0-migration if mode `Mode.OldImplicitResolution` is turned on as well.
1742+
* It is used to highlight differences between Scala 2 and 3 behavior.
17361743
*
1737-
* where `flip` changes covariant occurrences of contravariant type parameters to
1738-
* covariant ones. Intuitively `<:s` means subtyping `<:`, except that all arguments
1739-
* to contravariant parameters are compared as if they were covariant. E.g. given class
1744+
* - In Scala 3.0-3.4, the behavior is as follows: `T <:p U` iff there is an impliit conversion
1745+
* from `T` to `U`, or
17401746
*
1741-
* class Cmp[-X]
1747+
* flip(T) <: flip(U)
17421748
*
1743-
* `Cmp[T] <:s Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences
1744-
* of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:s Set[Cmp[T]]`,
1745-
* as usual, because `Set` is non-variant.
1749+
* where `flip` changes covariant occurrences of contravariant type parameters to
1750+
* covariant ones. Intuitively `<:p` means subtyping `<:`, except that all arguments
1751+
* to contravariant parameters are compared as if they were covariant. E.g. given class
17461752
*
1747-
* This relation might seem strange, but it models closely what happens for methods.
1748-
* Indeed, if we integrate the existing rules for methods into `<:s` we have now that
1753+
* class Cmp[-X]
17491754
*
1750-
* (T)R <:s (U)R
1755+
* `Cmp[T] <:p Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences
1756+
* of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:p Set[Cmp[T]]`,
1757+
* as usual, because `Set` is non-variant.
17511758
*
1752-
* iff
1759+
* - From Scala 3.5, `T <:p U` means `T <: U` or `T` convertible to `U`
1760+
* for overloading resolution (when `preferGeneral is false), and the opposite relation
1761+
* `U <: T` or `U convertible to `T` for implicit disambiguation between givens
1762+
* (when `preferGeneral` is true). For old-style implicit values, the 3.4 behavior is kept.
17531763
*
1754-
* T => R <:s U => R
1764+
* - In Scala 3.5-migration, use the 3.5 scheme normally, and the 3.4 scheme if
1765+
* `Mode.OldImplicitResolution` is on. This is used to highlight differences in the
1766+
* two resolution schemes.
17551767
*
1756-
* Also: If a compared type refers to a given or its module class, use
1768+
* Also and only for given resolution: If a compared type refers to a given or its module class, use
17571769
* the intersection of its parent classes instead.
17581770
*/
1759-
def isAsSpecificValueType(tp1: Type, tp2: Type)(using Context) =
1760-
if (ctx.mode.is(Mode.OldOverloadingResolution))
1771+
def isAsGoodValueType(tp1: Type, tp2: Type)(using Context) =
1772+
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1773+
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1774+
// Normal specificity test for overloading resolution (where `preferGeneral` is false)
1775+
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
17611776
isCompatible(tp1, tp2)
1762-
else {
1763-
val flip = new TypeMap {
1764-
def apply(t: Type) = t match {
1765-
case t @ AppliedType(tycon, args) =>
1766-
def mapArg(arg: Type, tparam: TypeParamInfo) =
1767-
if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType)
1768-
else arg
1769-
mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg)))
1770-
case _ => mapOver(t)
1771-
}
1772-
}
1773-
def prepare(tp: Type) = tp.stripTypeVar match {
1777+
else
1778+
def prepare(tp: Type) = tp.stripTypeVar match
17741779
case tp: NamedType if tp.symbol.is(Module) && tp.symbol.sourceModule.is(Given) =>
1775-
flip(tp.widen.widenToParents)
1776-
case _ => flip(tp)
1777-
}
1778-
(prepare(tp1) relaxed_<:< prepare(tp2)) || viewExists(tp1, tp2)
1779-
}
1780+
tp.widen.widenToParents
1781+
case _ =>
1782+
tp
1783+
1784+
val tp1p = prepare(tp1)
1785+
val tp2p = prepare(tp2)
1786+
1787+
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1788+
|| oldResolution
1789+
|| !compareGivens
1790+
then
1791+
// Intermediate rules: better means specialize, but map all type arguments downwards
1792+
// These are enabled for 3.0-3.4, and for all comparisons between old-style implicits,
1793+
// and in 3.5-migration when we compare with previous rules.
1794+
val flip = new TypeMap:
1795+
def apply(t: Type) = t match
1796+
case t @ AppliedType(tycon, args) =>
1797+
def mapArg(arg: Type, tparam: TypeParamInfo) =
1798+
if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType)
1799+
else arg
1800+
mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg)))
1801+
case _ => mapOver(t)
1802+
(flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2)
1803+
else
1804+
// New rules: better means generalize
1805+
(tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
1806+
end isAsGoodValueType
17801807

17811808
/** Widen the result type of synthetic given methods from the implementation class to the
17821809
* type that's implemented. Example
@@ -1809,8 +1836,8 @@ trait Applications extends Compatibility {
18091836

18101837
def compareWithTypes(tp1: Type, tp2: Type) = {
18111838
val ownerScore = compareOwner(alt1.symbol.maybeOwner, alt2.symbol.maybeOwner)
1812-
def winsType1 = isAsSpecific(alt1, tp1, alt2, tp2)
1813-
def winsType2 = isAsSpecific(alt2, tp2, alt1, tp1)
1839+
val winsType1 = isAsGood(alt1, tp1, alt2, tp2)
1840+
def winsType2 = isAsGood(alt2, tp2, alt1, tp1)
18141841

18151842
overload.println(i"compare($alt1, $alt2)? $tp1 $tp2 $ownerScore $winsType1 $winsType2")
18161843
if winsType1 && winsType2

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

+22-7
Original file line numberDiff line numberDiff line change
@@ -1111,8 +1111,8 @@ trait Implicits:
11111111
case result: SearchFailure if result.isAmbiguous =>
11121112
val deepPt = pt.deepenProto
11131113
if (deepPt ne pt) inferImplicit(deepPt, argument, span)
1114-
else if (migrateTo3 && !ctx.mode.is(Mode.OldOverloadingResolution))
1115-
withMode(Mode.OldOverloadingResolution)(inferImplicit(pt, argument, span)) match {
1114+
else if (migrateTo3 && !ctx.mode.is(Mode.OldImplicitResolution))
1115+
withMode(Mode.OldImplicitResolution)(inferImplicit(pt, argument, span)) match {
11161116
case altResult: SearchSuccess =>
11171117
report.migrationWarning(
11181118
result.reason.msg
@@ -1227,7 +1227,7 @@ trait Implicits:
12271227
assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType],
12281228
em"found: $argument: ${argument.tpe}, expected: $pt")
12291229

1230-
private def nestedContext() =
1230+
private def searchContext() =
12311231
ctx.fresh.setMode(ctx.mode &~ Mode.ImplicitsEnabled)
12321232

12331233
private def isCoherent = pt.isRef(defn.CanEqualClass)
@@ -1271,7 +1271,7 @@ trait Implicits:
12711271
else
12721272
val history = ctx.searchHistory.nest(cand, pt)
12731273
val typingCtx =
1274-
nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
1274+
searchContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
12751275
val result = typedImplicit(cand, pt, argument, span)(using typingCtx)
12761276
result match
12771277
case res: SearchSuccess =>
@@ -1296,9 +1296,24 @@ trait Implicits:
12961296
* 0 if neither alternative is preferred over the other
12971297
*/
12981298
def compareAlternatives(alt1: RefAndLevel, alt2: RefAndLevel): Int =
1299+
def comp(using Context) = explore(compare(alt1.ref, alt2.ref, preferGeneral = true))
12991300
if alt1.ref eq alt2.ref then 0
13001301
else if alt1.level != alt2.level then alt1.level - alt2.level
1301-
else explore(compare(alt1.ref, alt2.ref))(using nestedContext())
1302+
else
1303+
val cmp = comp(using searchContext())
1304+
if Feature.sourceVersion == SourceVersion.`3.5-migration` then
1305+
val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution))
1306+
if cmp != prev then
1307+
def choice(c: Int) = c match
1308+
case -1 => "the second alternative"
1309+
case 1 => "the first alternative"
1310+
case _ => "none - it's ambiguous"
1311+
report.warning(
1312+
em"""Change in given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref}
1313+
|Previous choice: ${choice(prev)}
1314+
|New choice : ${choice(cmp)}""", srcPos)
1315+
cmp
1316+
end compareAlternatives
13021317

13031318
/** If `alt1` is also a search success, try to disambiguate as follows:
13041319
* - If alt2 is preferred over alt1, pick alt2, otherwise return an
@@ -1334,8 +1349,8 @@ trait Implicits:
13341349
else
13351350
ctx.typerState
13361351

1337-
diff = inContext(ctx.withTyperState(comparisonState)):
1338-
compare(ref1, ref2)
1352+
diff = inContext(searchContext().withTyperState(comparisonState)):
1353+
compare(ref1, ref2, preferGeneral = true)
13391354
else // alt1 is a conversion, prefer extension alt2 over it
13401355
diff = -1
13411356
if diff < 0 then alt2

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ trait ImportSuggestions:
296296
var i = 0
297297
var diff = 0
298298
while i < filled && diff == 0 do
299-
diff = compare(ref, top(i))(using noImplicitsCtx)
299+
diff = compare(ref, top(i), preferGeneral = true)(using noImplicitsCtx)
300300
if diff > 0 then
301301
rest += top(i)
302302
top(i) = ref

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,22 @@ Condition (*) is new. It is necessary to ensure that the defined relation is tra
165165

166166
[//]: # todo: expand with precise rules
167167

168-
**9.** The following change is currently enabled in `-source future`:
168+
169+
**9.** Given disambiguation has changed. When comparing two givens that both match an expected type, we used to pick the most specific one, in alignment with
170+
overloading resolution. From Scala 3.5 on, we pick the most general one instead. Compiling with Scala 3.5-migration will print a warning in all cases where the preference has changed. Example:
171+
```scala
172+
class A
173+
class B extends A
174+
class C extends A
175+
176+
given A = A()
177+
given B = B()
178+
given C = C()
179+
180+
summon[A] // was ambiguous, will now return `given_A`
181+
```
182+
183+
**10.** The following change is currently enabled in `-source future`:
169184

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

tests/neg/given-triangle.check

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Error: tests/neg/given-triangle.scala:16:18 -------------------------------------------------------------------------
2+
16 |@main def Test = f // error
3+
| ^
4+
| Change in given search preference for A between alternatives (given_A : A) and (given_B : B)
5+
| Previous choice: the second alternative
6+
| New choice : the first alternative

tests/neg/given-triangle.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//> using options -source 3.5-migration -Xfatal-warnings
2+
3+
class A
4+
class B extends A
5+
class C extends A
6+
7+
given A = A()
8+
given B = B()
9+
given C = C()
10+
11+
def f(using a: A, b: B, c: C) =
12+
println(a.getClass)
13+
println(b.getClass)
14+
println(c.getClass)
15+
16+
@main def Test = f // error

0 commit comments

Comments
 (0)