diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 07791b9d046b..14ece3c6c594 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -50,8 +50,14 @@ object Implicits: def implicitName(using Context): TermName = alias } + /** Both search candidates and successes are references with a specific nesting level. */ + sealed trait RefAndLevel { + def ref: TermRef + def level: Int + } + /** An eligible implicit candidate, consisting of an implicit reference and a nesting level */ - case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) { + case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) extends RefAndLevel { def ref: TermRef = implicitRef.underlyingRef def isExtension = (kind & Candidate.Extension) != 0 @@ -385,7 +391,8 @@ object Implicits: * @param level The level where the reference was found * @param tstate The typer state to be committed if this alternative is chosen */ - case class SearchSuccess(tree: Tree, ref: TermRef, level: Int)(val tstate: TyperState, val gstate: GadtConstraint) extends SearchResult with Showable + case class SearchSuccess(tree: Tree, ref: TermRef, level: Int)(val tstate: TyperState, val gstate: GadtConstraint) + extends SearchResult with RefAndLevel with Showable /** A failed search */ case class SearchFailure(tree: Tree) extends SearchResult { @@ -1124,13 +1131,16 @@ trait Implicits: /** Search a list of eligible implicit references */ private def searchImplicit(eligible: List[Candidate], contextual: Boolean): SearchResult = - /** Compare previous success with reference and level to determine which one would be chosen, if - * an implicit starting with the reference was found. + /** Compare `alt1` with `alt2` to determine which one should be chosen. + * + * @return a number > 0 if `alt1` is preferred over `alt2` + * a number < 0 if `alt2` is preferred over `alt1` + * 0 if neither alternative is preferred over the other */ - def compareCandidate(prev: SearchSuccess, ref: TermRef, level: Int): Int = - if (prev.ref eq ref) 0 - else if (prev.level != level) prev.level - level - else explore(compare(prev.ref, ref))(using nestedContext()) + def compareAlternatives(alt1: RefAndLevel, alt2: RefAndLevel): Int = + if alt1.ref eq alt2.ref then 0 + else if alt1.level != alt2.level then alt1.level - alt2.level + else explore(compare(alt1.ref, alt2.ref))(using nestedContext()) /** If `alt1` is also a search success, try to disambiguate as follows: * - If alt2 is preferred over alt1, pick alt2, otherwise return an @@ -1138,7 +1148,7 @@ trait Implicits: */ def disambiguate(alt1: SearchResult, alt2: SearchSuccess) = alt1 match case alt1: SearchSuccess => - var diff = compareCandidate(alt1, alt2.ref, alt2.level) + var diff = compareAlternatives(alt1, alt2) assert(diff <= 0) // diff > 0 candidates should already have been eliminated in `rank` if diff == 0 then // Fall back: if both results are extension method applications, @@ -1159,18 +1169,6 @@ trait Implicits: else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument)) case _: SearchFailure => alt2 - /** Faced with an ambiguous implicits failure `fail`, try to find another - * alternative among `pending` that is strictly better than both ambiguous - * alternatives. If that fails, return `fail` - */ - def healAmbiguous(pending: List[Candidate], fail: SearchFailure) = { - val ambi = fail.reason.asInstanceOf[AmbiguousImplicits] - val newPending = pending.filter(cand => - compareCandidate(ambi.alt1, cand.ref, cand.level) < 0 && - compareCandidate(ambi.alt2, cand.ref, cand.level) < 0) - rank(newPending, fail, Nil).recoverWith(_ => fail) - } - /** Try to find a best matching implicit term among all the candidates in `pending`. * @param pending The list of candidates that remain to be tested * @param found The result obtained from previously tried candidates @@ -1184,7 +1182,7 @@ trait Implicits: * worse than the successful candidate. * If a trial failed: * - if the query term is a `Not[T]` treat it as a success, - * - otherwise, if the failure is an ambiguity, try to heal it (see @healAmbiguous) + * - otherwise, if the failure is an ambiguity, try to heal it (see `healAmbiguous`) * and return an ambiguous error otherwise. However, under Scala2 mode this is * treated as a simple failure, with a warning that semantics will change. * - otherwise add the failure to `rfailures` and continue testing the other candidates. @@ -1192,6 +1190,14 @@ trait Implicits: def rank(pending: List[Candidate], found: SearchResult, rfailures: List[SearchFailure]): SearchResult = pending match { case cand :: remaining => + /** To recover from an ambiguous implicit failure, we need to find a pending + * candidate that is strictly better than the failed candidate(s). + * If no such candidate is found, we propagate the ambiguity. + */ + def healAmbiguous(fail: SearchFailure, betterThanFailed: Candidate => Boolean) = + val newPending = remaining.filter(betterThanFailed) + rank(newPending, fail, Nil).recoverWith(_ => fail) + negateIfNot(tryImplicit(cand, contextual)) match { case fail: SearchFailure => if (fail.isAmbiguous) @@ -1200,7 +1206,11 @@ trait Implicits: if (result.isSuccess) warnAmbiguousNegation(fail.reason.asInstanceOf[AmbiguousImplicits]) result - else healAmbiguous(remaining, fail) + else + // The ambiguity happened in a nested search: to recover we + // need a candidate better than `cand` + healAmbiguous(fail, newCand => + compareAlternatives(newCand, cand) > 0) else rank(remaining, found, fail :: rfailures) case best: SearchSuccess => if (ctx.mode.is(Mode.ImplicitExploration) || isCoherent) @@ -1210,10 +1220,15 @@ trait Implicits: val newPending = if (retained eq found) || remaining.isEmpty then remaining else remaining.filterConserve(cand => - compareCandidate(retained, cand.ref, cand.level) <= 0) + compareAlternatives(retained, cand) <= 0) rank(newPending, retained, rfailures) case fail: SearchFailure => - healAmbiguous(remaining, fail) + // The ambiguity happened in the current search: to recover we + // need a candidate better than the two ambiguous alternatives. + val ambi = fail.reason.asInstanceOf[AmbiguousImplicits] + healAmbiguous(fail, newCand => + compareAlternatives(newCand, ambi.alt1) > 0 && + compareAlternatives(newCand, ambi.alt2) > 0) } } case nil => diff --git a/tests/pos/i9793.scala b/tests/pos/i9793.scala new file mode 100644 index 000000000000..ed83f5d1728b --- /dev/null +++ b/tests/pos/i9793.scala @@ -0,0 +1,20 @@ +trait Foo[F[_]] + +trait Bar[F[_]] extends Foo[F] +trait Baz[F[_]] extends Foo[F] + +case class Applied[F[_], A](a: F[A]) + + +object Applied extends AppliedLowPrio { + implicit def barApplied[F[_]: Baz]: Baz[({ type L[X] = Applied[F, X] })#L] = ??? +} + +trait AppliedLowPrio { + implicit def bazApplied[F[_]: Foo]: Foo[({ type L[X] = Applied[F, X] })#L] = ??? +} + + +object Test { + def test[F[_]](implicit bar: Bar[F], baz: Baz[F]) = implicitly[Foo[({ type L[X] = Applied[F, X] })#L]] +}