Skip to content

Commit

Permalink
Fix how implicit candidates are combined
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand committed Aug 1, 2023
1 parent 46fc22b commit 0908c3a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 3 deletions.
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ class PlainPrinter(_ctx: Context) extends Printer {
else if (pos.source.exists) s"${pos.source.file.name}:${pos.line + 1}"
else s"(no source file, offset = ${pos.span.point})"

def toText(cand: Candidate): Text =
"Candidate(" ~ toText(cand.ref) ~ ", " ~ Str("kind=" + cand.kind) ~ ", " ~ Str("lvl=" + cand.level) ~ ")"

def toText(result: SearchResult): Text = result match {
case result: SearchSuccess =>
"SearchSuccess: " ~ toText(result.ref) ~ " via " ~ toText(result.tree)
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/printing/Printer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Texts._, ast.Trees._
import Types.{Type, SingletonType, LambdaParam, NamedType},
Symbols.Symbol, Scopes.Scope, Constants.Constant,
Names.Name, Denotations._, Annotations.Annotation, Contexts.Context
import typer.Implicits.SearchResult
import typer.Implicits.*
import util.SourcePosition
import typer.ImportInfo

Expand Down Expand Up @@ -153,6 +153,9 @@ abstract class Printer {
/** Textual representation of source position */
def toText(pos: SourcePosition): Text

/** Textual representation of implicit candidates. */
def toText(cand: Candidate): Text

/** Textual representation of implicit search result */
def toText(result: SearchResult): Text

Expand Down
22 changes: 20 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ object Implicits:
}

/** An eligible implicit candidate, consisting of an implicit reference and a nesting level */
case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) extends RefAndLevel {
case class Candidate(implicitRef: ImplicitRef, kind: Candidate.Kind, level: Int) extends RefAndLevel with Showable {
def ref: TermRef = implicitRef.underlyingRef

def isExtension = (kind & Candidate.Extension) != 0
def isConversion = (kind & Candidate.Conversion) != 0

def toText(printer: Printer): Text = printer.toText(this)
}
object Candidate {
type Kind = Int
Expand Down Expand Up @@ -331,6 +333,7 @@ object Implicits:
else if outerEligible.isEmpty then ownEligible
else
def filter(xs: List[Candidate], remove: List[Candidate]) =
// Drop candidates that are shadowed by candidates in "remove"
val shadowed = remove.map(_.ref.implicitName).toSet
xs.filterConserve(cand => !shadowed.contains(cand.ref.implicitName))

Expand All @@ -342,7 +345,22 @@ object Implicits:
if !migrateTo3(using irefCtx) && level == outer.level && (preferDefinitions || preferNamedImport) then
// special cases: definitions beat imports, and named imports beat
// wildcard imports, provided both are in contexts with same scope
filter(ownEligible, outerEligible) ::: outerEligible

// Using only the outer candidates at the same level as us,
// remove from our own eligibles any shadowed candidate.
// This removes locally imported candidates from shadowing local definitions, (foo's in i18316)
// but without a remotely imported candidate removing a more locally imported candidates (mkFoo's in i18183)
val ownEligible1 = filter(ownEligible, outerEligible.filter(_.level == level))

// Remove, from the outer eligibles, any candidate shadowed by one of our own candidates,
// provided that the outer eligibles aren't at the same level (so actually shadows).
// This complements the filtering of our own eligible candidates, by removing candidates in the outer candidates
// that are low-level priority and shadowed by our candidates. E.g. the outer import Imp.mkFoo in i18183.
val shadowed = ownEligible.map(_.ref.implicitName).toSet
val outerEligible1 =
outerEligible.filterConserve(cand => cand.level == level || !shadowed.contains(cand.ref.implicitName))

ownEligible1 ::: outerEligible1
else
ownEligible ::: filter(outerEligible, ownEligible)

Expand Down
24 changes: 24 additions & 0 deletions tests/pos/i18316.orig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import scala.language.implicitConversions
object squerel {
trait EqualityExpression
object PrimitiveTypeMode:
implicit def intToTE(f: Int): TypedExpression[Int] = ???

trait TypedExpression[A1]:
def ===[A2](b: TypedExpression[A2]): EqualityExpression = ???
}

object scalactic {
trait TripleEqualsSupport:
class Equalizer[L](val leftSide: L):
def ===(rightSide: Any): Boolean = ???

trait TripleEquals extends TripleEqualsSupport:
implicit def convertToEqualizer[T](left: T): Equalizer[T] = ???
}

import squerel.PrimitiveTypeMode._ // remove to make code compile
object Test extends scalactic.TripleEquals {
import squerel.PrimitiveTypeMode._
val fails: squerel.EqualityExpression = 1 === 1
}
13 changes: 13 additions & 0 deletions tests/pos/i18316.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class R1
class R2

class Foo { def meth(x: Int): R1 = null }
class Bar { def meth(x: Int): R2 = null }

object Impl { implicit def mkFoo(i: Int): Foo = null }
trait Trait { implicit def mkBar(i: Int): Bar = null }

import Impl.mkFoo // remove to make code compile
object Test extends Trait:
import Impl.mkFoo
val fails: R1 = 1.meth(1)

0 comments on commit 0908c3a

Please sign in to comment.