Skip to content

Commit 35b5f7c

Browse files
committed
A left-biased variant for implicit/given pairs
We now use a left-biased scheme, as follows. From 3.6 on: - A given x: X is better than a given or implicit y: Y if y can be instantiated/widened to X. - An implicit x: X is better than a given or implicit y: Y if y can be instantiated to a supertype of X. - Use owner score for givens as a tie breaker if after all other tests we still have an ambiguity. This is not transitive, so we need a separate scheme to work around that. Other change: - Drop special handling of NotGiven in prioritization. The previous logic pretended to do so, but was ineffective.
1 parent 8064536 commit 35b5f7c

26 files changed

+424
-56
lines changed

compiler/src/dotty/tools/dotc/printing/Formatting.scala

+7-10
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package dotty.tools
22
package dotc
33
package printing
44

5-
import scala.language.unsafeNulls
6-
75
import scala.collection.mutable
86

97
import core.*
@@ -52,7 +50,11 @@ object Formatting {
5250
object ShowAny extends Show[Any]:
5351
def show(x: Any): Shown = x
5452

55-
class ShowImplicits3:
53+
class ShowImplicits4:
54+
given [X: Show]: Show[X | Null] with
55+
def show(x: X | Null) = if x == null then "null" else CtxShow(toStr(x.nn))
56+
57+
class ShowImplicits3 extends ShowImplicits4:
5658
given Show[Product] = ShowAny
5759

5860
class ShowImplicits2 extends ShowImplicits3:
@@ -77,15 +79,10 @@ object Formatting {
7779
given [K: Show, V: Show]: Show[Map[K, V]] with
7880
def show(x: Map[K, V]) =
7981
CtxShow(x.map((k, v) => s"${toStr(k)} => ${toStr(v)}"))
80-
end given
8182

8283
given [H: Show, T <: Tuple: Show]: Show[H *: T] with
8384
def show(x: H *: T) =
8485
CtxShow(toStr(x.head) *: toShown(x.tail).asInstanceOf[Tuple])
85-
end given
86-
87-
given [X: Show]: Show[X | Null] with
88-
def show(x: X | Null) = if x == null then "null" else CtxShow(toStr(x.nn))
8986

9087
given Show[FlagSet] with
9188
def show(x: FlagSet) = x.flagsString
@@ -148,8 +145,8 @@ object Formatting {
148145
private def treatArg(arg: Shown, suffix: String)(using Context): (String, String) = arg.runCtxShow match {
149146
case arg: Seq[?] if suffix.indexOf('%') == 0 && suffix.indexOf('%', 1) != -1 =>
150147
val end = suffix.indexOf('%', 1)
151-
val sep = StringContext.processEscapes(suffix.substring(1, end))
152-
(arg.mkString(sep), suffix.substring(end + 1))
148+
val sep = StringContext.processEscapes(suffix.substring(1, end).nn)
149+
(arg.mkString(sep), suffix.substring(end + 1).nn)
153150
case arg: Seq[?] =>
154151
(arg.map(showArg).mkString("[", ", ", "]"), suffix)
155152
case arg =>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -2988,7 +2988,7 @@ class MissingImplicitArgument(
29882988

29892989
/** Default error message for non-nested ambiguous implicits. */
29902990
def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) =
2991-
s"Ambiguous given instances: ${ambi.explanation}${location("of")}"
2991+
s"Ambiguous given instances: ${ambi.explanation}${location("of")}${ambi.priorityChangeWarningNote}"
29922992

29932993
/** Default error messages for non-ambiguous implicits, or nested ambiguous
29942994
* implicits.

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

+41-24
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,17 @@ trait Applications extends Compatibility {
17621762
else if sym2.is(Module) then compareOwner(sym1, cls2)
17631763
else 0
17641764

1765+
enum CompareScheme:
1766+
case Old // Normal specificity test for overloading resolution (where `preferGeneral` is false)
1767+
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
1768+
1769+
case Intermediate // Intermediate rules: better means specialize, but map all type arguments downwards
1770+
// These are enabled for 3.0-3.4, or if OldImplicitResolution
1771+
// is specified, and also for all comparisons between old-style implicits,
1772+
1773+
case New // New rules: better means generalize, givens (and extensions) always beat implicits
1774+
end CompareScheme
1775+
17651776
/** Compare two alternatives of an overloaded call or an implicit search.
17661777
*
17671778
* @param alt1, alt2 Non-overloaded references indicating the two choices
@@ -1788,6 +1799,15 @@ trait Applications extends Compatibility {
17881799
*/
17891800
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
17901801
record("resolveOverloaded.compare")
1802+
val scheme =
1803+
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1804+
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1805+
CompareScheme.Old
1806+
else if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1807+
|| oldResolution
1808+
|| alt1.symbol.is(Implicit) && alt2.symbol.is(Implicit)
1809+
then CompareScheme.Intermediate
1810+
else CompareScheme.New
17911811

17921812
/** Is alternative `alt1` with type `tp1` as good as alternative
17931813
* `alt2` with type `tp2` ?
@@ -1830,15 +1850,15 @@ trait Applications extends Compatibility {
18301850
isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
18311851
}
18321852
case _ => // (3)
1833-
def compareValues(tp1: Type, tp2: Type)(using Context) =
1834-
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit), alt2.symbol.is(Implicit))
1853+
def compareValues(tp2: Type)(using Context) =
1854+
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit))
18351855
tp2 match
18361856
case tp2: MethodType => true // (3a)
18371857
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
18381858
case tp2: PolyType => // (3b)
1839-
explore(compareValues(tp1, instantiateWithTypeVars(tp2)))
1859+
explore(compareValues(instantiateWithTypeVars(tp2)))
18401860
case _ => // 3b)
1841-
compareValues(tp1, tp2)
1861+
compareValues(tp2)
18421862
}
18431863

18441864
/** Test whether value type `tp1` is as good as value type `tp2`.
@@ -1876,9 +1896,8 @@ trait Applications extends Compatibility {
18761896
* Also and only for given resolution: If a compared type refers to a given or its module class, use
18771897
* the intersection of its parent classes instead.
18781898
*/
1879-
def isAsGoodValueType(tp1: Type, tp2: Type, alt1IsImplicit: Boolean, alt2IsImplicit: Boolean)(using Context): Boolean =
1880-
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1881-
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1899+
def isAsGoodValueType(tp1: Type, tp2: Type, alt1IsImplicit: Boolean)(using Context): Boolean =
1900+
if scheme == CompareScheme.Old then
18821901
// Normal specificity test for overloading resolution (where `preferGeneral` is false)
18831902
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
18841903
isCompatible(tp1, tp2)
@@ -1892,13 +1911,7 @@ trait Applications extends Compatibility {
18921911
val tp1p = prepare(tp1)
18931912
val tp2p = prepare(tp2)
18941913

1895-
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1896-
|| oldResolution
1897-
|| alt1IsImplicit && alt2IsImplicit
1898-
then
1899-
// Intermediate rules: better means specialize, but map all type arguments downwards
1900-
// These are enabled for 3.0-3.5, and for all comparisons between old-style implicits,
1901-
// and in 3.5 and 3.6-migration when we compare with previous rules.
1914+
if scheme == CompareScheme.Intermediate || alt1IsImplicit then
19021915
val flip = new TypeMap:
19031916
def apply(t: Type) = t match
19041917
case t @ AppliedType(tycon, args) =>
@@ -1909,9 +1922,7 @@ trait Applications extends Compatibility {
19091922
case _ => mapOver(t)
19101923
(flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2)
19111924
else
1912-
// New rules: better means generalize, givens (and extensions) always beat implicits
1913-
if alt1IsImplicit != alt2IsImplicit then alt2IsImplicit
1914-
else (tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
1925+
(tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
19151926
end isAsGoodValueType
19161927

19171928
/** Widen the result type of synthetic given methods from the implementation class to the
@@ -1982,13 +1993,19 @@ trait Applications extends Compatibility {
19821993
// alternatives are the same after following ExprTypes, pick one of them
19831994
// (prefer the one that is not a method, but that's arbitrary).
19841995
if alt1.widenExpr =:= alt2 then -1 else 1
1985-
else ownerScore match
1986-
case 1 => if winsType1 || !winsType2 then 1 else 0
1987-
case -1 => if winsType2 || !winsType1 then -1 else 0
1988-
case 0 =>
1989-
if winsType1 != winsType2 then if winsType1 then 1 else -1
1990-
else if alt1.symbol == alt2.symbol then comparePrefixes
1991-
else 0
1996+
else
1997+
// For new implicit resolution, take ownerscore as more significant than type resolution
1998+
// Reason: People use owner hierarchies to explicitly prioritize, we should not
1999+
// break that by changing implicit priority of types.
2000+
def drawOrOwner =
2001+
if scheme == CompareScheme.New then ownerScore else 0
2002+
ownerScore match
2003+
case 1 => if winsType1 || !winsType2 then 1 else drawOrOwner
2004+
case -1 => if winsType2 || !winsType1 then -1 else drawOrOwner
2005+
case 0 =>
2006+
if winsType1 != winsType2 then if winsType1 then 1 else -1
2007+
else if alt1.symbol == alt2.symbol then comparePrefixes
2008+
else 0
19922009
end compareWithTypes
19932010

19942011
if alt1.symbol.is(ConstructorProxy) && !alt2.symbol.is(ConstructorProxy) then -1

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

+33-6
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,11 @@ object Implicits:
549549
/** An ambiguous implicits failure */
550550
class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree, val nested: Boolean = false) extends SearchFailureType:
551551

552+
private[Implicits] var priorityChangeWarnings: List[Message] = Nil
553+
554+
def priorityChangeWarningNote(using Context): String =
555+
priorityChangeWarnings.map(msg => s"\n\nNote: $msg").mkString
556+
552557
def msg(using Context): Message =
553558
var str1 = err.refStr(alt1.ref)
554559
var str2 = err.refStr(alt2.ref)
@@ -1330,7 +1335,7 @@ trait Implicits:
13301335
if alt1.ref eq alt2.ref then 0
13311336
else if alt1.level != alt2.level then alt1.level - alt2.level
13321337
else
1333-
var cmp = comp(using searchContext())
1338+
val cmp = comp(using searchContext())
13341339
val sv = Feature.sourceVersion
13351340
if isWarnPriorityChangeVersion(sv) then
13361341
val prev = comp(using searchContext().addMode(Mode.OldImplicitResolution))
@@ -1345,13 +1350,21 @@ trait Implicits:
13451350
case _ => "none - it's ambiguous"
13461351
if sv.stable == SourceVersion.`3.5` then
13471352
warn(
1348-
em"""Given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref} will change
1353+
em"""Given search preference for $pt between alternatives
1354+
| ${alt1.ref}
1355+
|and
1356+
| ${alt2.ref}
1357+
|will change.
13491358
|Current choice : ${choice(prev)}
13501359
|New choice from Scala 3.6: ${choice(cmp)}""")
13511360
prev
13521361
else
13531362
warn(
1354-
em"""Change in given search preference for $pt between alternatives ${alt1.ref} and ${alt2.ref}
1363+
em"""Given search preference for $pt between alternatives
1364+
| ${alt1.ref}
1365+
|and
1366+
| ${alt2.ref}
1367+
|has changed.
13551368
|Previous choice : ${choice(prev)}
13561369
|New choice from Scala 3.6: ${choice(cmp)}""")
13571370
cmp
@@ -1610,9 +1623,23 @@ trait Implicits:
16101623
throw ex
16111624

16121625
val result = rank(sort(eligible), NoMatchingImplicitsFailure, Nil)
1613-
for (critical, msg) <- priorityChangeWarnings do
1614-
if result.found.exists(critical.contains(_)) then
1615-
report.warning(msg, srcPos)
1626+
1627+
// Issue all priority change warnings that can affect the result
1628+
val shownWarnings = priorityChangeWarnings.toList.collect:
1629+
case (critical, msg) if result.found.exists(critical.contains(_)) =>
1630+
msg
1631+
result match
1632+
case result: SearchFailure =>
1633+
result.reason match
1634+
case ambi: AmbiguousImplicits =>
1635+
// Make warnings part of error message because otherwise they are suppressed when
1636+
// the error is emitted.
1637+
ambi.priorityChangeWarnings = shownWarnings
1638+
case _ =>
1639+
case _ =>
1640+
for msg <- shownWarnings do
1641+
report.warning(msg, srcPos)
1642+
16161643
result
16171644
end searchImplicit
16181645

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

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class StringFormatterTest extends AbstractStringFormatterTest:
2323
@Test def flagsTup = check("(<static>,final)", i"${(JavaStatic, Final)}")
2424
@Test def seqOfTup2 = check("(final,given), (private,lazy)", i"${Seq((Final, Given), (Private, Lazy))}%, %")
2525
@Test def seqOfTup3 = check("(Foo,given, (right is approximated))", i"${Seq((Foo, Given, TypeComparer.ApproxState.None.addHigh))}%, %")
26+
@Test def tupleNull = check("(1,null)", i"${(1, null: String | Null)}")
2627

2728
class StorePrinter extends Printer:
2829
var string: String = "<never set>"

tests/neg/given-triangle.check

+8
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@
22
15 |@main def Test = f // error
33
| ^
44
|Ambiguous given instances: both given instance given_B and given instance given_C match type A of parameter a of method f
5+
|
6+
|Note: Given search preference for A between alternatives
7+
| (given_A : A)
8+
|and
9+
| (given_B : B)
10+
|will change.
11+
|Current choice : the second alternative
12+
|New choice from Scala 3.6: the first alternative

tests/neg/i21212.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- [E172] Type Error: tests/neg/i21212.scala:8:52 ----------------------------------------------------------------------
2+
8 | def test2(using a2: A)(implicit b2: B) = summon[A] // error: ambiguous
3+
| ^
4+
|Ambiguous given instances: both parameter b2 and parameter a2 match type Minimization.A of parameter x of method summon in object Predef

tests/neg/i21212.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
object Minimization:
3+
4+
trait A
5+
trait B extends A
6+
7+
def test1(using a1: A)(using b1: B) = summon[A] // picks (most general) a1
8+
def test2(using a2: A)(implicit b2: B) = summon[A] // error: ambiguous
9+
def test3(implicit a3: A, b3: B) = summon[A] // picks (most specific) b3
10+
11+
end Minimization

tests/neg/i21303/JavaEnum.java

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public enum JavaEnum { ABC, DEF, GHI }

tests/neg/i21303/Test.scala

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//> using options -source 3.6-migration
2+
import scala.deriving.Mirror
3+
import scala.compiletime.*
4+
import scala.reflect.ClassTag
5+
import scala.annotation.implicitNotFound
6+
7+
8+
trait TSType[T]
9+
object TSType extends DefaultTSTypes with TSTypeMacros
10+
11+
trait TSNamedType[T] extends TSType[T]
12+
13+
trait DefaultTSTypes extends JavaTSTypes
14+
trait JavaTSTypes {
15+
given javaEnumTSType[E <: java.lang.Enum[E]: ClassTag]: TSNamedType[E] = ???
16+
}
17+
object DefaultTSTypes extends DefaultTSTypes
18+
trait TSTypeMacros {
19+
inline given [T: Mirror.Of]: TSType[T] = derived[T]
20+
inline def derived[T](using m: Mirror.Of[T]): TSType[T] = {
21+
val elemInstances = summonAll[m.MirroredElemTypes]
22+
???
23+
}
24+
25+
private inline def summonAll[T <: Tuple]: List[TSType[_]] = {
26+
inline erasedValue[T] match {
27+
case _: EmptyTuple => Nil
28+
case _: (t *: ts) => summonInline[TSType[t]] :: summonAll[ts]
29+
}
30+
}
31+
}
32+
33+
@main def Test = summon[TSType[JavaEnum]] // error

tests/neg/i2974.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
trait Foo[-T]
3+
trait Bar[-T] extends Foo[T]
4+
5+
object Test {
6+
7+
locally:
8+
implicit val fa: Foo[Int] = ???
9+
implicit val ba: Bar[Int] = ???
10+
summon[Foo[Int]] // ok
11+
12+
locally:
13+
implicit val fa: Foo[Int] = ???
14+
implicit val ba: Bar[Any] = ???
15+
summon[Foo[Int]] // error: ambiguous
16+
}

tests/neg/scala-uri.check

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- [E172] Type Error: tests/neg/scala-uri.scala:30:59 ------------------------------------------------------------------
2+
30 |@main def Test = summon[QueryKeyValue[(String, None.type)]] // error
3+
| ^
4+
|No best given instance of type QueryKeyValue[(String, None.type)] was found for parameter x of method summon in object Predef.
5+
|I found:
6+
|
7+
| QueryKeyValue.tuple2QueryKeyValue[String, None.type](QueryKey.stringQueryKey,
8+
| QueryValue.optionQueryValue[A](
9+
| /* ambiguous: both given instance stringQueryValue in trait QueryValueInstances1 and given instance noneQueryValue in trait QueryValueInstances1 match type QueryValue[A] */
10+
| summon[QueryValue[A]]
11+
| )
12+
| )
13+
|
14+
|But both given instance stringQueryValue in trait QueryValueInstances1 and given instance noneQueryValue in trait QueryValueInstances1 match type QueryValue[A].

tests/neg/scala-uri.scala

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import scala.language.implicitConversions
2+
3+
trait QueryKey[A]
4+
object QueryKey extends QueryKeyInstances
5+
sealed trait QueryKeyInstances:
6+
given stringQueryKey: QueryKey[String] = ???
7+
8+
trait QueryValue[-A]
9+
object QueryValue extends QueryValueInstances
10+
sealed trait QueryValueInstances1:
11+
given stringQueryValue: QueryValue[String] = ???
12+
given noneQueryValue: QueryValue[None.type] = ???
13+
// The noneQueryValue makes no sense at this priority. Since QueryValue
14+
// is contravariant, QueryValue[None.type] is always better than QueryValue[Option[A]]
15+
// no matter whether it's old or new resolution. So taking both owner and type
16+
// score into account, it's always a draw. With the new disambiguation, we prefer
17+
// the optionQueryValue[A], which gives an ambiguity down the road, because we don't
18+
// know what the wrapped type A is. Previously, we preferred QueryValue[None.type]
19+
// because it is unconditional. The solution is to put QueryValue[None.type] in the
20+
// same trait as QueryValue[Option[A]], as is shown in pos/scala-uri.scala.
21+
22+
sealed trait QueryValueInstances extends QueryValueInstances1:
23+
given optionQueryValue[A: QueryValue]: QueryValue[Option[A]] = ???
24+
25+
trait QueryKeyValue[A]
26+
object QueryKeyValue:
27+
given tuple2QueryKeyValue[K: QueryKey, V: QueryValue]: QueryKeyValue[(K, V)] = ???
28+
29+
30+
@main def Test = summon[QueryKeyValue[(String, None.type)]] // error

0 commit comments

Comments
 (0)