Skip to content

Properly recover from nested ambiguous implicit search failures #9937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 40 additions & 25 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1124,21 +1131,24 @@ 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
* ambiguous implicits error.
*/
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,
Expand All @@ -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
Expand All @@ -1184,14 +1182,22 @@ 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.
*/
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)
Expand All @@ -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)
Expand All @@ -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 =>
Expand Down
20 changes: 20 additions & 0 deletions tests/pos/i9793.scala
Original file line number Diff line number Diff line change
@@ -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]]
}