Skip to content

Commit

Permalink
Prefer extensions over conversions for member selection
Browse files Browse the repository at this point in the history
Fixes #19715
  • Loading branch information
odersky committed Feb 17, 2024
1 parent f95b57c commit c79bd93
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 29 deletions.
59 changes: 30 additions & 29 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1308,35 +1308,36 @@ trait Implicits:
case alt1: SearchSuccess =>
var diff = compareAlternatives(alt1, alt2)
assert(diff <= 0) // diff > 0 candidates should already have been eliminated in `rank`
if diff == 0 && alt1.isExtension && alt2.isExtension then
// Fall back: if both results are extension method applications,
// compare the extension methods instead of their wrappers.
def stripExtension(alt: SearchSuccess) = methPart(stripApply(alt.tree)).tpe
(stripExtension(alt1), stripExtension(alt2)) match
case (ref1: TermRef, ref2: TermRef) =>
// ref1 and ref2 might refer to type variables owned by
// alt1.tstate and alt2.tstate respectively, to compare the
// alternatives correctly we need a TyperState that includes
// constraints from both sides, see
// tests/*/extension-specificity2.scala for test cases.
val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint
val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint
def exploreState(alt: SearchSuccess): TyperState =
alt.tstate.fresh(committable = false)
val comparisonState =
if constraintsIn1 && constraintsIn2 then
exploreState(alt1).mergeConstraintWith(alt2.tstate)
else if constraintsIn1 then
exploreState(alt1)
else if constraintsIn2 then
exploreState(alt2)
else
ctx.typerState

diff = inContext(ctx.withTyperState(comparisonState)) {
compare(ref1, ref2)
}
case _ =>
if diff == 0 && alt2.isExtension then
if alt1.isExtension then
// Fall back: if both results are extension method applications,
// compare the extension methods instead of their wrappers.
def stripExtension(alt: SearchSuccess) = methPart(stripApply(alt.tree)).tpe
(stripExtension(alt1), stripExtension(alt2)) match
case (ref1: TermRef, ref2: TermRef) =>
// ref1 and ref2 might refer to type variables owned by
// alt1.tstate and alt2.tstate respectively, to compare the
// alternatives correctly we need a TyperState that includes
// constraints from both sides, see
// tests/*/extension-specificity2.scala for test cases.
val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint
val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint
def exploreState(alt: SearchSuccess): TyperState =
alt.tstate.fresh(committable = false)
val comparisonState =
if constraintsIn1 && constraintsIn2 then
exploreState(alt1).mergeConstraintWith(alt2.tstate)
else if constraintsIn1 then
exploreState(alt1)
else if constraintsIn2 then
exploreState(alt2)
else
ctx.typerState

diff = inContext(ctx.withTyperState(comparisonState)):
compare(ref1, ref2)
else // alt1 is a conversion, prefer extension alt2 over it
diff = -1
if diff < 0 then alt2
else if diff > 0 then alt1
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
Expand Down
15 changes: 15 additions & 0 deletions tests/pos/i19715.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Tup():
def app(n: Int): String = "a"

class NT(t: Tup):
def toTup = t
object NT:
extension (x: NT)
def app(n: Int): Boolean = true
given Conversion[NT, Tup] = _.toTup

def test =
val nt = new NT(Tup())
val x = nt.app(3)
val _: Boolean = x

0 comments on commit c79bd93

Please sign in to comment.