Skip to content

Commit 7f46c25

Browse files
authored
Merge pull request #9937 from dotty-staging/nested-ambiguous-2
Properly recover from nested ambiguous implicit search failures
2 parents e2ade80 + fc89087 commit 7f46c25

File tree

2 files changed

+60
-25
lines changed

2 files changed

+60
-25
lines changed

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

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ object Implicits:
5050
def implicitName(using Context): TermName = alias
5151
}
5252

53+
/** Both search candidates and successes are references with a specific nesting level. */
54+
sealed trait RefAndLevel {
55+
def ref: TermRef
56+
def level: Int
57+
}
58+
5359
/** An eligible implicit candidate, consisting of an implicit reference and a nesting level */
54-
case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) {
60+
case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) extends RefAndLevel {
5561
def ref: TermRef = implicitRef.underlyingRef
5662

5763
def isExtension = (kind & Candidate.Extension) != 0
@@ -385,7 +391,8 @@ object Implicits:
385391
* @param level The level where the reference was found
386392
* @param tstate The typer state to be committed if this alternative is chosen
387393
*/
388-
case class SearchSuccess(tree: Tree, ref: TermRef, level: Int)(val tstate: TyperState, val gstate: GadtConstraint) extends SearchResult with Showable
394+
case class SearchSuccess(tree: Tree, ref: TermRef, level: Int)(val tstate: TyperState, val gstate: GadtConstraint)
395+
extends SearchResult with RefAndLevel with Showable
389396

390397
/** A failed search */
391398
case class SearchFailure(tree: Tree) extends SearchResult {
@@ -1124,21 +1131,24 @@ trait Implicits:
11241131
/** Search a list of eligible implicit references */
11251132
private def searchImplicit(eligible: List[Candidate], contextual: Boolean): SearchResult =
11261133

1127-
/** Compare previous success with reference and level to determine which one would be chosen, if
1128-
* an implicit starting with the reference was found.
1134+
/** Compare `alt1` with `alt2` to determine which one should be chosen.
1135+
*
1136+
* @return a number > 0 if `alt1` is preferred over `alt2`
1137+
* a number < 0 if `alt2` is preferred over `alt1`
1138+
* 0 if neither alternative is preferred over the other
11291139
*/
1130-
def compareCandidate(prev: SearchSuccess, ref: TermRef, level: Int): Int =
1131-
if (prev.ref eq ref) 0
1132-
else if (prev.level != level) prev.level - level
1133-
else explore(compare(prev.ref, ref))(using nestedContext())
1140+
def compareAlternatives(alt1: RefAndLevel, alt2: RefAndLevel): Int =
1141+
if alt1.ref eq alt2.ref then 0
1142+
else if alt1.level != alt2.level then alt1.level - alt2.level
1143+
else explore(compare(alt1.ref, alt2.ref))(using nestedContext())
11341144

11351145
/** If `alt1` is also a search success, try to disambiguate as follows:
11361146
* - If alt2 is preferred over alt1, pick alt2, otherwise return an
11371147
* ambiguous implicits error.
11381148
*/
11391149
def disambiguate(alt1: SearchResult, alt2: SearchSuccess) = alt1 match
11401150
case alt1: SearchSuccess =>
1141-
var diff = compareCandidate(alt1, alt2.ref, alt2.level)
1151+
var diff = compareAlternatives(alt1, alt2)
11421152
assert(diff <= 0) // diff > 0 candidates should already have been eliminated in `rank`
11431153
if diff == 0 then
11441154
// Fall back: if both results are extension method applications,
@@ -1159,18 +1169,6 @@ trait Implicits:
11591169
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument))
11601170
case _: SearchFailure => alt2
11611171

1162-
/** Faced with an ambiguous implicits failure `fail`, try to find another
1163-
* alternative among `pending` that is strictly better than both ambiguous
1164-
* alternatives. If that fails, return `fail`
1165-
*/
1166-
def healAmbiguous(pending: List[Candidate], fail: SearchFailure) = {
1167-
val ambi = fail.reason.asInstanceOf[AmbiguousImplicits]
1168-
val newPending = pending.filter(cand =>
1169-
compareCandidate(ambi.alt1, cand.ref, cand.level) < 0 &&
1170-
compareCandidate(ambi.alt2, cand.ref, cand.level) < 0)
1171-
rank(newPending, fail, Nil).recoverWith(_ => fail)
1172-
}
1173-
11741172
/** Try to find a best matching implicit term among all the candidates in `pending`.
11751173
* @param pending The list of candidates that remain to be tested
11761174
* @param found The result obtained from previously tried candidates
@@ -1184,14 +1182,22 @@ trait Implicits:
11841182
* worse than the successful candidate.
11851183
* If a trial failed:
11861184
* - if the query term is a `Not[T]` treat it as a success,
1187-
* - otherwise, if the failure is an ambiguity, try to heal it (see @healAmbiguous)
1185+
* - otherwise, if the failure is an ambiguity, try to heal it (see `healAmbiguous`)
11881186
* and return an ambiguous error otherwise. However, under Scala2 mode this is
11891187
* treated as a simple failure, with a warning that semantics will change.
11901188
* - otherwise add the failure to `rfailures` and continue testing the other candidates.
11911189
*/
11921190
def rank(pending: List[Candidate], found: SearchResult, rfailures: List[SearchFailure]): SearchResult =
11931191
pending match {
11941192
case cand :: remaining =>
1193+
/** To recover from an ambiguous implicit failure, we need to find a pending
1194+
* candidate that is strictly better than the failed candidate(s).
1195+
* If no such candidate is found, we propagate the ambiguity.
1196+
*/
1197+
def healAmbiguous(fail: SearchFailure, betterThanFailed: Candidate => Boolean) =
1198+
val newPending = remaining.filter(betterThanFailed)
1199+
rank(newPending, fail, Nil).recoverWith(_ => fail)
1200+
11951201
negateIfNot(tryImplicit(cand, contextual)) match {
11961202
case fail: SearchFailure =>
11971203
if (fail.isAmbiguous)
@@ -1200,7 +1206,11 @@ trait Implicits:
12001206
if (result.isSuccess)
12011207
warnAmbiguousNegation(fail.reason.asInstanceOf[AmbiguousImplicits])
12021208
result
1203-
else healAmbiguous(remaining, fail)
1209+
else
1210+
// The ambiguity happened in a nested search: to recover we
1211+
// need a candidate better than `cand`
1212+
healAmbiguous(fail, newCand =>
1213+
compareAlternatives(newCand, cand) > 0)
12041214
else rank(remaining, found, fail :: rfailures)
12051215
case best: SearchSuccess =>
12061216
if (ctx.mode.is(Mode.ImplicitExploration) || isCoherent)
@@ -1210,10 +1220,15 @@ trait Implicits:
12101220
val newPending =
12111221
if (retained eq found) || remaining.isEmpty then remaining
12121222
else remaining.filterConserve(cand =>
1213-
compareCandidate(retained, cand.ref, cand.level) <= 0)
1223+
compareAlternatives(retained, cand) <= 0)
12141224
rank(newPending, retained, rfailures)
12151225
case fail: SearchFailure =>
1216-
healAmbiguous(remaining, fail)
1226+
// The ambiguity happened in the current search: to recover we
1227+
// need a candidate better than the two ambiguous alternatives.
1228+
val ambi = fail.reason.asInstanceOf[AmbiguousImplicits]
1229+
healAmbiguous(fail, newCand =>
1230+
compareAlternatives(newCand, ambi.alt1) > 0 &&
1231+
compareAlternatives(newCand, ambi.alt2) > 0)
12171232
}
12181233
}
12191234
case nil =>

tests/pos/i9793.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
trait Foo[F[_]]
2+
3+
trait Bar[F[_]] extends Foo[F]
4+
trait Baz[F[_]] extends Foo[F]
5+
6+
case class Applied[F[_], A](a: F[A])
7+
8+
9+
object Applied extends AppliedLowPrio {
10+
implicit def barApplied[F[_]: Baz]: Baz[({ type L[X] = Applied[F, X] })#L] = ???
11+
}
12+
13+
trait AppliedLowPrio {
14+
implicit def bazApplied[F[_]: Foo]: Foo[({ type L[X] = Applied[F, X] })#L] = ???
15+
}
16+
17+
18+
object Test {
19+
def test[F[_]](implicit bar: Bar[F], baz: Baz[F]) = implicitly[Foo[({ type L[X] = Applied[F, X] })#L]]
20+
}

0 commit comments

Comments
 (0)