From beef3697c12413b9cb5d0939ce59525d41a64b1f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 17 Feb 2024 15:56:35 +0100 Subject: [PATCH] Prefer extensions over conversions for member selection Fixes #19715 [Cherry-picked c79bd93b72e1de2eda7f794aa8ee34f26af8bf1b] --- .../dotty/tools/dotc/typer/Implicits.scala | 59 ++++++++++--------- tests/pos/i19715.scala | 15 +++++ 2 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 tests/pos/i19715.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index c043492164b5..cfda041745e4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -1275,35 +1275,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) diff --git a/tests/pos/i19715.scala b/tests/pos/i19715.scala new file mode 100644 index 000000000000..91aeda5c1698 --- /dev/null +++ b/tests/pos/i19715.scala @@ -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 +