Skip to content

Commit eda2e06

Browse files
authored
Change rules for given prioritization (#19300)
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.
2 parents c5f2064 + 8a3854f commit eda2e06

23 files changed

+345
-84
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ enum SourceVersion:
1111
case `3.3-migration`, `3.3`
1212
case `3.4-migration`, `3.4`
1313
case `3.5-migration`, `3.5`
14+
case `3.6-migration`, `3.6`
1415
// !!! Keep in sync with scala.runtime.stdlibPatches.language !!!
1516
case `future-migration`, `future`
1617

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

Lines changed: 12 additions & 7 deletions
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
*/
@@ -133,6 +136,8 @@ object Mode {
133136
/** We are typing the body of an inline method */
134137
val InlineableBody: Mode = newMode(21, "InlineableBody")
135138

139+
val NewGivenRules: Mode = newMode(22, "NewGivenRules")
140+
136141
/** We are synthesizing the receiver of an extension method */
137142
val SynthesizeExtMethodReceiver: Mode = newMode(23, "SynthesizeExtMethodReceiver")
138143

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

Lines changed: 84 additions & 55 deletions
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}
@@ -1709,6 +1709,12 @@ trait Applications extends Compatibility {
17091709
/** Compare two alternatives of an overloaded call or an implicit search.
17101710
*
17111711
* @param alt1, alt2 Non-overloaded references indicating the two choices
1712+
* @param preferGeneral When comparing two value types, prefer the more general one
1713+
* over the more specific one iff `preferGeneral` is true.
1714+
* `preferGeneral` is set to `true` when we compare two given values, since
1715+
* then we want the most general evidence that matches the target
1716+
* type. It is set to `false` for overloading resolution, when we want the
1717+
* most specific type instead.
17121718
* @return 1 if 1st alternative is preferred over 2nd
17131719
* -1 if 2nd alternative is preferred over 1st
17141720
* 0 if neither alternative is preferred over the other
@@ -1724,27 +1730,26 @@ trait Applications extends Compatibility {
17241730
* an alternative that takes more implicit parameters wins over one
17251731
* that takes fewer.
17261732
*/
1727-
def compare(alt1: TermRef, alt2: TermRef)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
1733+
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
17281734
record("resolveOverloaded.compare")
17291735

1730-
/** Is alternative `alt1` with type `tp1` as specific as alternative
1736+
/** Is alternative `alt1` with type `tp1` as good as alternative
17311737
* `alt2` with type `tp2` ?
17321738
*
1733-
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as specific as `alt2`
1739+
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as good as `alt2`
17341740
* if `alt1` is nullary or `alt2` is applicable to arguments (p1, ..., pn) of
17351741
* types T1,...,Tn. If the last parameter `pn` has a vararg type T*, then
17361742
* `alt1` must be applicable to arbitrary numbers of `T` parameters (which
17371743
* implies that it must be a varargs method as well).
17381744
* 2. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as
1739-
* specific as `alt2` of type `tp2` if T is as specific as `tp2` under the
1745+
* good as `alt2` of type `tp2` if T is as good as `tp2` under the
17401746
* assumption that for i = 1,...,n each ai is an abstract type name bounded
17411747
* from below by Li and from above by Ui.
17421748
* 3. A member of any other type `tp1` is:
1743-
* a. always as specific as a method or a polymorphic method.
1744-
* b. as specific as a member of any other type `tp2` if `tp1` is compatible
1745-
* with `tp2`.
1749+
* a. always as good as a method or a polymorphic method.
1750+
* b. as good as a member of any other type `tp2` if `asGoodValueType(tp1, tp2) = true`
17461751
*/
1747-
def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) {
1752+
def isAsGood(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) {
17481753
tp1 match
17491754
case tp1: MethodType => // (1)
17501755
tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType]
@@ -1766,69 +1771,94 @@ trait Applications extends Compatibility {
17661771
fullyDefinedType(tp1Params, "type parameters of alternative", alt1.symbol.srcPos)
17671772

17681773
val tparams = newTypeParams(alt1.symbol, tp1.paramNames, EmptyFlags, tp1.instantiateParamInfos(_))
1769-
isAsSpecific(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
1774+
isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
17701775
}
17711776
case _ => // (3)
1777+
def isGiven(alt: TermRef) =
1778+
alt1.symbol.is(Given) && alt.symbol != defn.NotGivenClass
1779+
def compareValues(tp1: Type, tp2: Type)(using Context) =
1780+
isAsGoodValueType(tp1, tp2, isGiven(alt1), isGiven(alt2))
17721781
tp2 match
17731782
case tp2: MethodType => true // (3a)
17741783
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
17751784
case tp2: PolyType => // (3b)
1776-
explore(isAsSpecificValueType(tp1, instantiateWithTypeVars(tp2)))
1785+
explore(compareValues(tp1, instantiateWithTypeVars(tp2)))
17771786
case _ => // 3b)
1778-
isAsSpecificValueType(tp1, tp2)
1787+
compareValues(tp1, tp2)
17791788
}
17801789

1781-
/** Test whether value type `tp1` is as specific as value type `tp2`.
1782-
* Let's abbreviate this to `tp1 <:s tp2`.
1783-
* Previously, `<:s` was the same as `<:`. This behavior is still
1784-
* available under mode `Mode.OldOverloadingResolution`. The new behavior
1785-
* is different, however. Here, `T <:s U` iff
1790+
/** Test whether value type `tp1` is as good as value type `tp2`.
1791+
* Let's abbreviate this to `tp1 <:p tp2`. The behavior depends on the Scala version
1792+
* and mode.
17861793
*
1787-
* flip(T) <: flip(U)
1794+
* - In Scala 2, `<:p` was the same as `<:`. This behavior is still
1795+
* available in 3.0-migration if mode `Mode.OldImplicitResolution` is turned on as well.
1796+
* It is used to highlight differences between Scala 2 and 3 behavior.
17881797
*
1789-
* where `flip` changes covariant occurrences of contravariant type parameters to
1790-
* covariant ones. Intuitively `<:s` means subtyping `<:`, except that all arguments
1791-
* to contravariant parameters are compared as if they were covariant. E.g. given class
1798+
* - In Scala 3.0-3.5, the behavior is as follows: `T <:p U` iff there is an impliit conversion
1799+
* from `T` to `U`, or
17921800
*
1793-
* class Cmp[-X]
1801+
* flip(T) <: flip(U)
17941802
*
1795-
* `Cmp[T] <:s Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences
1796-
* of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:s Set[Cmp[T]]`,
1797-
* as usual, because `Set` is non-variant.
1803+
* where `flip` changes covariant occurrences of contravariant type parameters to
1804+
* covariant ones. Intuitively `<:p` means subtyping `<:`, except that all arguments
1805+
* to contravariant parameters are compared as if they were covariant. E.g. given class
17981806
*
1799-
* This relation might seem strange, but it models closely what happens for methods.
1800-
* Indeed, if we integrate the existing rules for methods into `<:s` we have now that
1807+
* class Cmp[-X]
18011808
*
1802-
* (T)R <:s (U)R
1809+
* `Cmp[T] <:p Cmp[U]` if `T <: U`. On the other hand, non-variant occurrences
1810+
* of parameters are not affected. So `T <: U` would imply `Set[Cmp[U]] <:p Set[Cmp[T]]`,
1811+
* as usual, because `Set` is non-variant.
18031812
*
1804-
* iff
1813+
* - From Scala 3.6, `T <:p U` means `T <: U` or `T` convertible to `U`
1814+
* for overloading resolution (when `preferGeneral is false), and the opposite relation
1815+
* `U <: T` or `U convertible to `T` for implicit disambiguation between givens
1816+
* (when `preferGeneral` is true). For old-style implicit values, the 3.4 behavior is kept.
1817+
* If one of the alternatives is a given and the other is an implicit, the given wins.
18051818
*
1806-
* T => R <:s U => R
1819+
* - In Scala 3.5 and Scala 3.6-migration, we issue a warning if the result under
1820+
* Scala 3.6 differ wrt to the old behavior up to 3.5.
18071821
*
1808-
* Also: If a compared type refers to a given or its module class, use
1822+
* Also and only for given resolution: If a compared type refers to a given or its module class, use
18091823
* the intersection of its parent classes instead.
18101824
*/
1811-
def isAsSpecificValueType(tp1: Type, tp2: Type)(using Context) =
1812-
if (ctx.mode.is(Mode.OldOverloadingResolution))
1825+
def isAsGoodValueType(tp1: Type, tp2: Type, alt1isGiven: Boolean, alt2isGiven: Boolean)(using Context): Boolean =
1826+
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1827+
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1828+
// Normal specificity test for overloading resolution (where `preferGeneral` is false)
1829+
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
18131830
isCompatible(tp1, tp2)
1814-
else {
1815-
val flip = new TypeMap {
1816-
def apply(t: Type) = t match {
1817-
case t @ AppliedType(tycon, args) =>
1818-
def mapArg(arg: Type, tparam: TypeParamInfo) =
1819-
if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType)
1820-
else arg
1821-
mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg)))
1822-
case _ => mapOver(t)
1823-
}
1824-
}
1825-
def prepare(tp: Type) = tp.stripTypeVar match {
1831+
else
1832+
def prepare(tp: Type) = tp.stripTypeVar match
18261833
case tp: NamedType if tp.symbol.is(Module) && tp.symbol.sourceModule.is(Given) =>
1827-
flip(tp.widen.widenToParents)
1828-
case _ => flip(tp)
1829-
}
1830-
(prepare(tp1) relaxed_<:< prepare(tp2)) || viewExists(tp1, tp2)
1831-
}
1834+
tp.widen.widenToParents
1835+
case _ =>
1836+
tp
1837+
1838+
val tp1p = prepare(tp1)
1839+
val tp2p = prepare(tp2)
1840+
1841+
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1842+
|| oldResolution
1843+
|| !alt1isGiven && !alt2isGiven
1844+
then
1845+
// Intermediate rules: better means specialize, but map all type arguments downwards
1846+
// These are enabled for 3.0-3.5, and for all comparisons between old-style implicits,
1847+
// and in 3.5 amd 3.6-migration when we compare with previous rules.
1848+
val flip = new TypeMap:
1849+
def apply(t: Type) = t match
1850+
case t @ AppliedType(tycon, args) =>
1851+
def mapArg(arg: Type, tparam: TypeParamInfo) =
1852+
if (variance > 0 && tparam.paramVarianceSign < 0) defn.FunctionNOf(arg :: Nil, defn.UnitType)
1853+
else arg
1854+
mapOver(t.derivedAppliedType(tycon, args.zipWithConserve(tycon.typeParams)(mapArg)))
1855+
case _ => mapOver(t)
1856+
(flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2)
1857+
else
1858+
// New rules: better means generalize, givens always beat implicits
1859+
if alt1isGiven != alt2isGiven then alt1isGiven
1860+
else (tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
1861+
end isAsGoodValueType
18321862

18331863
/** Widen the result type of synthetic given methods from the implementation class to the
18341864
* type that's implemented. Example
@@ -1880,17 +1910,16 @@ trait Applications extends Compatibility {
18801910
def comparePrefixes =
18811911
val pre1 = widenPrefix(alt1)
18821912
val pre2 = widenPrefix(alt2)
1883-
val winsPrefix1 = isAsSpecificValueType(pre1, pre2)
1884-
val winsPrefix2 = isAsSpecificValueType(pre2, pre1)
1913+
val winsPrefix1 = isCompatible(pre1, pre2)
1914+
val winsPrefix2 = isCompatible(pre2, pre1)
18851915
if winsPrefix1 == winsPrefix2 then 0
18861916
else if winsPrefix1 then 1
18871917
else -1
18881918

18891919
def compareWithTypes(tp1: Type, tp2: Type) =
18901920
val ownerScore = compareOwner(alt1.symbol.maybeOwner, alt2.symbol.maybeOwner)
1891-
1892-
val winsType1 = isAsSpecific(alt1, tp1, alt2, tp2)
1893-
val winsType2 = isAsSpecific(alt2, tp2, alt1, tp1)
1921+
val winsType1 = isAsGood(alt1, tp1, alt2, tp2)
1922+
val winsType2 = isAsGood(alt2, tp2, alt1, tp1)
18941923

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

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

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ object Implicits:
531531
|must be more specific than $target""" :: Nil
532532

533533
override def msg(using Context) =
534-
super.msg.append(i"\nThe expected type $target is not specific enough, so no search was attempted")
534+
super.msg.append("\nThe expected type $target is not specific enough, so no search was attempted")
535535

536536
override def toString = s"TooUnspecific"
537537
end TooUnspecific
@@ -1110,8 +1110,8 @@ trait Implicits:
11101110
case result: SearchFailure if result.isAmbiguous =>
11111111
val deepPt = pt.deepenProto
11121112
if (deepPt ne pt) inferImplicit(deepPt, argument, span)
1113-
else if (migrateTo3 && !ctx.mode.is(Mode.OldOverloadingResolution))
1114-
withMode(Mode.OldOverloadingResolution)(inferImplicit(pt, argument, span)) match {
1113+
else if (migrateTo3 && !ctx.mode.is(Mode.OldImplicitResolution))
1114+
withMode(Mode.OldImplicitResolution)(inferImplicit(pt, argument, span)) match {
11151115
case altResult: SearchSuccess =>
11161116
report.migrationWarning(
11171117
result.reason.msg
@@ -1226,7 +1226,7 @@ trait Implicits:
12261226
assert(argument.isEmpty || argument.tpe.isValueType || argument.tpe.isInstanceOf[ExprType],
12271227
em"found: $argument: ${argument.tpe}, expected: $pt")
12281228

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

12321232
private def isCoherent = pt.isRef(defn.CanEqualClass)
@@ -1270,7 +1270,7 @@ trait Implicits:
12701270
else
12711271
val history = ctx.searchHistory.nest(cand, pt)
12721272
val typingCtx =
1273-
nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
1273+
searchContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history)
12741274
val result = typedImplicit(cand, pt, argument, span)(using typingCtx)
12751275
result match
12761276
case res: SearchSuccess =>
@@ -1293,11 +1293,44 @@ trait Implicits:
12931293
* @return a number > 0 if `alt1` is preferred over `alt2`
12941294
* a number < 0 if `alt2` is preferred over `alt1`
12951295
* 0 if neither alternative is preferred over the other
1296+
* The behavior depends on the source version
1297+
* before 3.5: compare with preferGeneral = false
1298+
* 3.5: compare twice with preferGeneral = false and true, warning if result is different,
1299+
* return old result with preferGeneral = false
1300+
* 3.6-migration: compare twice with preferGeneral = false and true, warning if result is different,
1301+
* return new result with preferGeneral = true
1302+
* 3.6 and higher: compare with preferGeneral = true
1303+
*
12961304
*/
12971305
def compareAlternatives(alt1: RefAndLevel, alt2: RefAndLevel): Int =
1306+
def comp(using Context) = explore(compare(alt1.ref, alt2.ref, preferGeneral = true))
12981307
if alt1.ref eq alt2.ref then 0
12991308
else if alt1.level != alt2.level then alt1.level - alt2.level
1300-
else explore(compare(alt1.ref, alt2.ref))(using nestedContext())
1309+
else
1310+
var cmp = comp(using searchContext())
1311+
val sv = Feature.sourceVersion
1312+
if sv == SourceVersion.`3.5` || sv == SourceVersion.`3.6-migration` then
1313+
val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution))
1314+
if cmp != prev then
1315+
def choice(c: Int) = c match
1316+
case -1 => "the second alternative"
1317+
case 1 => "the first alternative"
1318+
case _ => "none - it's ambiguous"
1319+
if sv == SourceVersion.`3.5` then
1320+
report.warning(
1321+
em"""Given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref} will change
1322+
|Current choice : ${choice(prev)}
1323+
|New choice from Scala 3.6: ${choice(cmp)}""", srcPos)
1324+
prev
1325+
else
1326+
report.warning(
1327+
em"""Change in given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref}
1328+
|Previous choice : ${choice(prev)}
1329+
|New choice from Scala 3.6: ${choice(cmp)}""", srcPos)
1330+
cmp
1331+
else cmp
1332+
else cmp
1333+
end compareAlternatives
13011334

13021335
/** If `alt1` is also a search success, try to disambiguate as follows:
13031336
* - If alt2 is preferred over alt1, pick alt2, otherwise return an
@@ -1307,7 +1340,9 @@ trait Implicits:
13071340
case alt1: SearchSuccess =>
13081341
var diff = compareAlternatives(alt1, alt2)
13091342
assert(diff <= 0) // diff > 0 candidates should already have been eliminated in `rank`
1310-
if diff == 0 && alt2.isExtension then
1343+
if diff == 0 && alt1.ref =:= alt2.ref then
1344+
diff = 1 // See i12951 for a test where this happens
1345+
else if diff == 0 && alt2.isExtension then
13111346
if alt1.isExtension then
13121347
// Fall back: if both results are extension method applications,
13131348
// compare the extension methods instead of their wrappers.
@@ -1333,8 +1368,8 @@ trait Implicits:
13331368
else
13341369
ctx.typerState
13351370

1336-
diff = inContext(ctx.withTyperState(comparisonState)):
1337-
compare(ref1, ref2)
1371+
diff = inContext(searchContext().withTyperState(comparisonState)):
1372+
compare(ref1, ref2, preferGeneral = true)
13381373
else // alt1 is a conversion, prefer extension alt2 over it
13391374
diff = -1
13401375
if diff < 0 then alt2

0 commit comments

Comments
 (0)