Skip to content
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

Fix how implicit candidates are combined #18321

Merged
merged 2 commits into from
Aug 22, 2023
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
7 changes: 7 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,13 @@ 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 =
"Cand("
~ toTextRef(cand.ref)
~ (if cand.isConversion then " conv" else "")
~ (if cand.isExtension then " ext" else "")
~ Str(" L" + 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
36 changes: 21 additions & 15 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ProtoTypes._
import ErrorReporting._
import Inferencing.{fullyDefinedType, isFullyDefined}
import Scopes.newScope
import Typer.BindingPrec, BindingPrec.*
import transform.TypeUtils._
import Hashable._
import util.{EqHashMap, Stats}
Expand All @@ -49,17 +50,19 @@ object Implicits:
}

/** Both search candidates and successes are references with a specific nesting level. */
sealed trait RefAndLevel {
sealed trait RefAndLevel extends Showable {
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) 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 @@ -326,25 +329,28 @@ object Implicits:
(this eq finalImplicits) || (outerImplicits eqn finalImplicits)
}

def bindingPrec: BindingPrec =
if isImport then if ctx.importInfo.uncheckedNN.isWildcardImport then WildImport else NamedImport else Definition

private def combineEligibles(ownEligible: List[Candidate], outerEligible: List[Candidate]): List[Candidate] =
if ownEligible.isEmpty then outerEligible
else if outerEligible.isEmpty then ownEligible
else
def filter(xs: List[Candidate], remove: List[Candidate]) =
val shadowed = remove.map(_.ref.implicitName).toSet
xs.filterConserve(cand => !shadowed.contains(cand.ref.implicitName))

val ownNames = mutable.Set(ownEligible.map(_.ref.implicitName)*)
val outer = outerImplicits.uncheckedNN
def isWildcardImport(using Context) = ctx.importInfo.nn.isWildcardImport
def preferDefinitions = isImport && !outer.isImport
def preferNamedImport = isWildcardImport && !isWildcardImport(using outer.irefCtx)

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
if !migrateTo3(using irefCtx) && level == outer.level && outer.bindingPrec.beats(bindingPrec) then
val keptOuters = outerEligible.filterConserve: cand =>
if ownNames.contains(cand.ref.implicitName) then
val keepOuter = cand.level == level
if keepOuter then ownNames -= cand.ref.implicitName
keepOuter
else true
val keptOwn = ownEligible.filterConserve: cand =>
ownNames.contains(cand.ref.implicitName)
keptOwn ::: keptOuters
else
ownEligible ::: filter(outerEligible, ownEligible)
ownEligible ::: outerEligible.filterConserve: cand =>
!ownNames.contains(cand.ref.implicitName)

def uncachedEligible(tp: Type)(using Context): List[Candidate] =
Stats.record("uncached eligible")
Expand Down
9 changes: 6 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ object Typer {
case NothingBound, PackageClause, WildImport, NamedImport, Inheritance, Definition

def isImportPrec = this == NamedImport || this == WildImport

/** special cases: definitions beat imports, and named imports beat
* wildcard imports, provided both are in contexts with same scope */
def beats(prevPrec: BindingPrec): Boolean =
this == Definition || this == NamedImport && prevPrec == WildImport
}

/** Assert tree has a position, unless it is empty or a typed splice */
Expand Down Expand Up @@ -226,9 +231,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
def checkNewOrShadowed(found: Type, newPrec: BindingPrec, scala2pkg: Boolean = false)(using Context): Type =
if !previous.exists || TypeComparer.isSameRef(previous, found) then
found
else if (prevCtx.scope eq ctx.scope)
&& (newPrec == Definition || newPrec == NamedImport && prevPrec == WildImport)
then
else if (prevCtx.scope eq ctx.scope) && newPrec.beats(prevPrec) then
// special cases: definitions beat imports, and named imports beat
// wildcard imports, provided both are in contexts with same scope
found
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)