Skip to content

Commit 078fcb7

Browse files
Merge pull request #9065 from dotty-staging/fix-#9058b
Fix #9058: Make implicit search preference relation transitive
2 parents 2116281 + e756d62 commit 078fcb7

File tree

2 files changed

+57
-11
lines changed

2 files changed

+57
-11
lines changed

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

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,16 +1347,53 @@ trait Applications extends Compatibility {
13471347
* @return 1 if `sym1` properly derives from `sym2`
13481348
* -1 if `sym2` properly derives from `sym1`
13491349
* 0 otherwise
1350-
* Module classes also inherit the relationship from their companions.
1350+
* Module classes also inherit the relationship from their companions. This means,
1351+
* if no direct derivation exists between `sym1` and `sym2` also perform the following
1352+
* tests:
1353+
* - If both sym1 and sym1 are module classes that have companion classes, compare the companions
1354+
* - If sym1 is a module class with a companion, and sym2 is a normal class or trait, compare
1355+
* the companion with sym2.
1356+
*
1357+
* Note that this makes `compareOwner(_, _) > 0` not transitive! For instance:
1358+
*
1359+
* class A extends B
1360+
* object A
1361+
* class B
1362+
* object B extends C
1363+
*
1364+
* Then compareOwner(A$, B$) = 1 and compareOwner(B$, C) == 1, but
1365+
* compareOwner(A$, C) == 0.
13511366
*/
13521367
def compareOwner(sym1: Symbol, sym2: Symbol)(using Context): Int =
1353-
if (sym1 == sym2) 0
1354-
else if (sym1 isSubClass sym2) 1
1355-
else if (sym2 isSubClass sym1) -1
1356-
else if (sym2.is(Module)) compareOwner(sym1, sym2.companionClass)
1357-
else if (sym1.is(Module)) compareOwner(sym1.companionClass, sym2)
1368+
if sym1 == sym2 then 0
1369+
else if sym1.isSubClass(sym2) then 1
1370+
else if sym2.isSubClass(sym1) then -1
1371+
else if sym1.is(Module) then
1372+
compareOwner(sym1.companionClass, if sym2.is(Module) then sym2.companionClass else sym2)
13581373
else 0
13591374

1375+
/** A version of `compareOwner` that is transitive, to be used in sorting
1376+
* It would be nice if we could use this as the general method for comparing
1377+
* owners, but unfortunately this does not compile all existsing code.
1378+
* An example is `enrich-gentraversable.scala`. Here we have
1379+
*
1380+
* trait BuildFrom...
1381+
* object BuildFrom extends BuildFromLowPriority1
1382+
*
1383+
* and we need to pick an implicit in BuildFrom over BuildFromLowPriority1
1384+
* the rules in `compareOwner` give us that, but the rules in `isSubOwner`
1385+
* don't.
1386+
* So we need to stick with `compareOwner` for backwards compatibility, even
1387+
* though it is arguably broken. We can still use `isSubOwner` for sorting
1388+
* since that is just a performance optimization, so if the two methods
1389+
* don't agree sometimes that's OK.
1390+
*/
1391+
def isSubOwner(sym1: Symbol, sym2: Symbol)(using Context): Boolean =
1392+
if sym1.is(Module) && sym1.companionClass.exists then
1393+
isSubOwner(sym1.companionClass, if sym2.is(Module) then sym2.companionClass else sym2)
1394+
else
1395+
sym1 != sym2 && sym1.isSubClass(sym2)
1396+
13601397
/** Compare to alternatives of an overloaded call or an implicit search.
13611398
*
13621399
* @param alt1, alt2 Non-overloaded references indicating the two choices

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,14 +1169,11 @@ trait Implicits { self: Typer =>
11691169
if (level1 < level2) return false
11701170
val sym1 = cand1.ref.symbol
11711171
val sym2 = cand2.ref.symbol
1172-
val ownerScore = compareOwner(sym1.maybeOwner, sym2.maybeOwner)
1173-
if (ownerScore > 0) return true
1174-
if (ownerScore < 0) return false
11751172
val arity1 = sym1.info.firstParamTypes.length
11761173
val arity2 = sym2.info.firstParamTypes.length
11771174
if (arity1 < arity2) return true
11781175
if (arity1 > arity2) return false
1179-
false
1176+
isSubOwner(sym1, sym2)
11801177
}
11811178

11821179
/** Sort list of implicit references according to `prefer`.
@@ -1190,7 +1187,19 @@ trait Implicits { self: Typer =>
11901187
if (prefer(e2, e1)) e2 :: e1 :: Nil
11911188
else eligible
11921189
case _ =>
1193-
eligible.sortWith(prefer)
1190+
try eligible.sortWith(prefer)
1191+
catch case ex: IllegalArgumentException =>
1192+
// diagnostic to see what went wrong
1193+
for
1194+
e1 <- eligible
1195+
e2 <- eligible
1196+
if prefer(e1, e2)
1197+
e3 <- eligible
1198+
if prefer(e2, e3) && !prefer(e1, e3)
1199+
do
1200+
val es = List(e1, e2, e3)
1201+
println(i"transitivity violated for $es%, %\n ${es.map(_.implicitRef.underlyingRef.symbol.showLocated)}%, %")
1202+
throw ex
11941203
}
11951204

11961205
rank(sort(eligible), NoMatchingImplicitsFailure, Nil)

0 commit comments

Comments
 (0)