Skip to content

Commit 1f554d5

Browse files
oderskyWojciechMazur
authored andcommitted
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 32f5200 commit 1f554d5

25 files changed

+445
-44
lines changed

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

Lines changed: 7 additions & 10 deletions
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2985,7 +2985,7 @@ class MissingImplicitArgument(
29852985

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

29902990
/** Default error messages for non-ambiguous implicits, or nested ambiguous
29912991
* implicits.

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

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,17 @@ trait Applications extends Compatibility {
17481748
else if sym2.is(Module) then compareOwner(sym1, cls2)
17491749
else 0
17501750

1751+
enum CompareScheme:
1752+
case Old // Normal specificity test for overloading resolution (where `preferGeneral` is false)
1753+
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
1754+
1755+
case Intermediate // Intermediate rules: better means specialize, but map all type arguments downwards
1756+
// These are enabled for 3.0-3.4, or if OldImplicitResolution
1757+
// is specified, and also for all comparisons between old-style implicits,
1758+
1759+
case New // New rules: better means generalize, givens (and extensions) always beat implicits
1760+
end CompareScheme
1761+
17511762
/** Compare two alternatives of an overloaded call or an implicit search.
17521763
*
17531764
* @param alt1, alt2 Non-overloaded references indicating the two choices
@@ -1774,6 +1785,15 @@ trait Applications extends Compatibility {
17741785
*/
17751786
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
17761787
record("resolveOverloaded.compare")
1788+
val scheme =
1789+
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1790+
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1791+
CompareScheme.Old
1792+
else if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1793+
|| oldResolution
1794+
|| alt1.symbol.is(Implicit) && alt2.symbol.is(Implicit)
1795+
then CompareScheme.Intermediate
1796+
else CompareScheme.New
17771797

17781798
/** Is alternative `alt1` with type `tp1` as good as alternative
17791799
* `alt2` with type `tp2` ?
@@ -1816,15 +1836,15 @@ trait Applications extends Compatibility {
18161836
isAsGood(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
18171837
}
18181838
case _ => // (3)
1819-
def compareValues(tp1: Type, tp2: Type)(using Context) =
1820-
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit), alt2.symbol.is(Implicit))
1839+
def compareValues(tp2: Type)(using Context) =
1840+
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit))
18211841
tp2 match
18221842
case tp2: MethodType => true // (3a)
18231843
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
18241844
case tp2: PolyType => // (3b)
1825-
explore(compareValues(tp1, instantiateWithTypeVars(tp2)))
1845+
explore(compareValues(instantiateWithTypeVars(tp2)))
18261846
case _ => // 3b)
1827-
compareValues(tp1, tp2)
1847+
compareValues(tp2)
18281848
}
18291849

18301850
/** Test whether value type `tp1` is as good as value type `tp2`.
@@ -1862,9 +1882,8 @@ trait Applications extends Compatibility {
18621882
* Also and only for given resolution: If a compared type refers to a given or its module class, use
18631883
* the intersection of its parent classes instead.
18641884
*/
1865-
def isAsGoodValueType(tp1: Type, tp2: Type, alt1IsImplicit: Boolean, alt2IsImplicit: Boolean)(using Context): Boolean =
1866-
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
1867-
if !preferGeneral || Feature.migrateTo3 && oldResolution then
1885+
def isAsGoodValueType(tp1: Type, tp2: Type, alt1IsImplicit: Boolean)(using Context): Boolean =
1886+
if scheme == CompareScheme.Old then
18681887
// Normal specificity test for overloading resolution (where `preferGeneral` is false)
18691888
// and in mode Scala3-migration when we compare with the old Scala 2 rules.
18701889
isCompatible(tp1, tp2)
@@ -1878,13 +1897,7 @@ trait Applications extends Compatibility {
18781897
val tp1p = prepare(tp1)
18791898
val tp2p = prepare(tp2)
18801899

1881-
if Feature.sourceVersion.isAtMost(SourceVersion.`3.4`)
1882-
|| oldResolution
1883-
|| alt1IsImplicit && alt2IsImplicit
1884-
then
1885-
// Intermediate rules: better means specialize, but map all type arguments downwards
1886-
// These are enabled for 3.0-3.5, and for all comparisons between old-style implicits,
1887-
// and in 3.5 and 3.6-migration when we compare with previous rules.
1900+
if scheme == CompareScheme.Intermediate || alt1IsImplicit then
18881901
val flip = new TypeMap:
18891902
def apply(t: Type) = t match
18901903
case t @ AppliedType(tycon, args) =>
@@ -1895,9 +1908,7 @@ trait Applications extends Compatibility {
18951908
case _ => mapOver(t)
18961909
(flip(tp1p) relaxed_<:< flip(tp2p)) || viewExists(tp1, tp2)
18971910
else
1898-
// New rules: better means generalize, givens (and extensions) always beat implicits
1899-
if alt1IsImplicit != alt2IsImplicit then alt2IsImplicit
1900-
else (tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
1911+
(tp2p relaxed_<:< tp1p) || viewExists(tp2, tp1)
19011912
end isAsGoodValueType
19021913

19031914
/** Widen the result type of synthetic given methods from the implementation class to the
@@ -1968,13 +1979,19 @@ trait Applications extends Compatibility {
19681979
// alternatives are the same after following ExprTypes, pick one of them
19691980
// (prefer the one that is not a method, but that's arbitrary).
19701981
if alt1.widenExpr =:= alt2 then -1 else 1
1971-
else ownerScore match
1972-
case 1 => if winsType1 || !winsType2 then 1 else 0
1973-
case -1 => if winsType2 || !winsType1 then -1 else 0
1974-
case 0 =>
1975-
if winsType1 != winsType2 then if winsType1 then 1 else -1
1976-
else if alt1.symbol == alt2.symbol then comparePrefixes
1977-
else 0
1982+
else
1983+
// For new implicit resolution, take ownerscore as more significant than type resolution
1984+
// Reason: People use owner hierarchies to explicitly prioritize, we should not
1985+
// break that by changing implicit priority of types.
1986+
def drawOrOwner =
1987+
if scheme == CompareScheme.New then ownerScore else 0
1988+
ownerScore match
1989+
case 1 => if winsType1 || !winsType2 then 1 else drawOrOwner
1990+
case -1 => if winsType2 || !winsType1 then -1 else drawOrOwner
1991+
case 0 =>
1992+
if winsType1 != winsType2 then if winsType1 then 1 else -1
1993+
else if alt1.symbol == alt2.symbol then comparePrefixes
1994+
else 0
19781995
end compareWithTypes
19791996

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

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

Lines changed: 33 additions & 6 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//> using options -source:3.6
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public enum JavaEnum { ABC, DEF, GHI }

tests/neg/i21303/Test.scala

Lines changed: 33 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 30 additions & 0 deletions
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)