Skip to content

Commit

Permalink
Fix regression in overload resolution picking eta-expanded method
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand committed Oct 26, 2024
1 parent f7f51ed commit 6331820
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 33 deletions.
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -802,11 +802,11 @@ object Contexts {
final def retractMode(mode: Mode): c.type = c.setMode(c.mode &~ mode)
}

/** Run `op` with a pool-allocated context that has an ExporeTyperState. */
/** Run `op` with a pool-allocated context that has an ExploreTyperState. */
inline def explore[T](inline op: Context ?=> T)(using Context): T =
exploreInFreshCtx(op)

/** Run `op` with a pool-allocated FreshContext that has an ExporeTyperState. */
/** Run `op` with a pool-allocated FreshContext that has an ExploreTyperState. */
inline def exploreInFreshCtx[T](inline op: FreshContext ?=> T)(using Context): T =
val pool = ctx.base.exploreContextPool
val nestedCtx = pool.next()
Expand Down Expand Up @@ -931,7 +931,7 @@ object Contexts {
FreshContext(ctx.base).init(ctx, ctx)

private var inUse: Int = 0
private var pool = new mutable.ArrayBuffer[FreshContext]
private val pool = new mutable.ArrayBuffer[FreshContext]

def next()(using Context): FreshContext =
val base = ctx.base
Expand Down
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,17 @@ object Types extends TypeUtils {
NoType
}

/** Follow proxies and approximate type paramrefs by their upper bound
* in the current constraint in order to figure out robustly
* whether an expected type is some sort of function type.
*/
def underlyingApplied(using Context): Type = this.stripTypeVar match
case tp: RefinedType => tp
case tp: AppliedType => tp
case tp: TypeParamRef => TypeComparer.bounds(tp).hi.underlyingApplied
case tp: TypeProxy => tp.superType.underlyingApplied
case _ => this

/** The iterator of underlying types as long as type is a TypeProxy.
* Useful for diagnostics
*/
Expand Down
43 changes: 25 additions & 18 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1803,7 +1803,8 @@ trait Applications extends Compatibility {
* an alternative that takes more implicit parameters wins over one
* that takes fewer.
*/
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false)(using Context): Int = trace(i"compare($alt1, $alt2)", overload) {
def compare(alt1: TermRef, alt2: TermRef, preferGeneral: Boolean = false, needsEta: Boolean = false)(using Context): Int =
trace(i"compare($alt1, $alt2)", overload) {
record("resolveOverloaded.compare")
val scheme =
val oldResolution = ctx.mode.is(Mode.OldImplicitResolution)
Expand All @@ -1818,30 +1819,34 @@ trait Applications extends Compatibility {
/** Is alternative `alt1` with type `tp1` as good as alternative
* `alt2` with type `tp2` ?
*
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as good as `alt2`
* if `alt1` is nullary or `alt2` is applicable to arguments (p1, ..., pn) of
* types T1,...,Tn. If the last parameter `pn` has a vararg type T*, then
* 1. A method `alt1` of type `(p1: T1, ..., pn: Tn)U` is as good as `alt2` if:
* a. `alt1` is nullary, or
* b. `alt2` is applicable to arguments (p1, ..., pn) of types T1,...,Tn, or
* c. eta-expanded `alt1` is as good as `alt2` (when needing eta-expansion).
* When testing method applicability (1b), if the last parameter `pn` has a vararg type T*, then
* `alt1` must be applicable to arbitrary numbers of `T` parameters (which
* implies that it must be a varargs method as well).
* 2. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as
* good as `alt2` of type `tp2` if T is as good as `tp2` under the
* assumption that for i = 1,...,n each ai is an abstract type name bounded
* from below by Li and from above by Ui.
* 3. A member of any other type `tp1` is:
* a. always as good as a method or a polymorphic method.
* b. as good as a member of any other type `tp2` if `asGoodValueType(tp1, tp2) = true`
* a. as good as an eta-expanded `tp2` method (when needing eta-expansion)
* b. always as good as a (not eta-expanded) method or a polymorphic method.
* c. as good as a member of any other type `tp2` if `asGoodValueType(tp1, tp2) = true`
*/
def isAsGood(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsGood $tp1 $tp2", overload) {
tp1 match
case tp1: MethodType => // (1)
tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType]
tp1.paramInfos.isEmpty && tp2.isInstanceOf[MethodOrPoly] // (1a)
|| {
if tp1.isVarArgsMethod then
tp2.isVarArgsMethod
&& isApplicableMethodRef(alt2, tp1.paramInfos.map(_.repeatedToSingle), WildcardType, ArgMatch.Compatible)
&& isApplicableMethodRef(alt2, tp1.paramInfos.map(_.repeatedToSingle), WildcardType, ArgMatch.Compatible) // (1b)
else
isApplicableMethodRef(alt2, tp1.paramInfos, WildcardType, ArgMatch.Compatible)
isApplicableMethodRef(alt2, tp1.paramInfos, WildcardType, ArgMatch.Compatible) // (1b)
}
|| needsEta && !tp2.isInstanceOf[MethodOrPoly] && isAsGood(alt1, tp1.toFunctionType(), alt2, tp2) // (1c)
case tp1: PolyType => // (2)
inContext(ctx.fresh.setExploreTyperState()) {
// Fully define the PolyType parameters so that the infos of the
Expand All @@ -1859,11 +1864,12 @@ trait Applications extends Compatibility {
def compareValues(tp2: Type)(using Context) =
isAsGoodValueType(tp1, tp2, alt1.symbol.is(Implicit))
tp2 match
case tp2: MethodType => true // (3a)
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
case tp2: PolyType => // (3b)
case tp2: MethodType if needsEta => isAsGood(alt1, tp1, alt2, tp2.toFunctionType()) // (3a)
case tp2: MethodType => true // (3b)
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3b)
case tp2: PolyType => // (3c)
explore(compareValues(instantiateWithTypeVars(tp2)))
case _ => // 3b)
case _ => // (3c)
compareValues(tp2)
}

Expand Down Expand Up @@ -2037,21 +2043,21 @@ trait Applications extends Compatibility {
}
end compare

def narrowMostSpecific(alts: List[TermRef])(using Context): List[TermRef] = {
def narrowMostSpecific(alts: List[TermRef], needsEta: Boolean)(using Context): List[TermRef] = {
record("narrowMostSpecific")
alts match {
case Nil => alts
case _ :: Nil => alts
case alt1 :: alt2 :: Nil =>
compare(alt1, alt2) match {
compare(alt1, alt2, needsEta = needsEta) match {
case 1 => alt1 :: Nil
case -1 => alt2 :: Nil
case 0 => alts
}
case alt :: alts1 =>
def survivors(previous: List[TermRef], alts: List[TermRef]): List[TermRef] = alts match {
case alt :: alts1 =>
compare(previous.head, alt) match {
compare(previous.head, alt, needsEta = needsEta) match {
case 1 => survivors(previous, alts1)
case -1 => survivors(alt :: previous.tail, alts1)
case 0 => survivors(alt :: previous, alts1)
Expand All @@ -2061,7 +2067,7 @@ trait Applications extends Compatibility {
val best :: rest = survivors(alt :: Nil, alts1): @unchecked
def asGood(alts: List[TermRef]): List[TermRef] = alts match {
case alt :: alts1 =>
if (compare(alt, best) < 0) asGood(alts1) else alt :: asGood(alts1)
if (compare(alt, best, needsEta = needsEta) < 0) asGood(alts1) else alt :: asGood(alts1)
case nil =>
Nil
}
Expand Down Expand Up @@ -2374,7 +2380,8 @@ trait Applications extends Compatibility {
// If `pt` is erroneous, don't try to go further; report the error in `pt` instead.
candidates
else
val found = narrowMostSpecific(candidates)
val needsEta = defn.isFunctionNType(pt.underlyingApplied)
val found = narrowMostSpecific(candidates, needsEta)
if found.length <= 1 then found
else
val deepPt = pt.deepenProto
Expand Down
13 changes: 1 addition & 12 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4471,17 +4471,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
tree
}

// Follow proxies and approximate type paramrefs by their upper bound
// in the current constraint in order to figure out robustly
// whether an expected type is some sort of function type.
def underlyingApplied(tp: Type): Type = tp.stripTypeVar match {
case tp: RefinedType => tp
case tp: AppliedType => tp
case tp: TypeParamRef => underlyingApplied(TypeComparer.bounds(tp).hi)
case tp: TypeProxy => underlyingApplied(tp.superType)
case _ => tp
}

// If the expected type is a selection of an extension method, deepen it
// to also propagate the argument type (which is the receiver we have
// typechecked already). This is needed for i8311.scala. Doing so
Expand All @@ -4494,7 +4483,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
case _ => pt

def adaptNoArgs(wtp: Type): Tree = {
val ptNorm = underlyingApplied(pt)
val ptNorm = pt.underlyingApplied
def functionExpected = defn.isFunctionNType(ptNorm)
def needsEta = pt.revealIgnored match
case _: SingletonType | _: FunOrPolyProto => false
Expand Down
21 changes: 21 additions & 0 deletions tests/pos/i21727.min.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.language.implicitConversions

type UUID = String
object MyId:
def fromUUID[F[_]: Functor: UUIDGen]: F[String] =
toFunctorOps(UUIDGen[F].randomUUID).map(fromUUID) // error
private def fromUUID(id: UUID): String = ???

object UUIDGen:
def apply[F[_]](implicit ev: UUIDGen[F]): UUIDGen[F] = ev
trait UUIDGen[F[_]]:
def randomUUID: F[UUID]

trait Functor[F[_]]
implicit def toFunctorOps[F[_], A](target: F[A])(implicit tc: Functor[F]): Ops[F, A] { type TypeClassType = Functor[F]} =
new Ops[F, A] { type TypeClassType = Functor[F] }

trait Ops[F[_], A] {
type TypeClassType <: Functor[F]
def map[B](f: A => B): F[B] = ???
}
18 changes: 18 additions & 0 deletions tests/pos/i21727.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
trait ExMap1[K1, +V1] extends PartialFunction[K1, V1]
trait ExMap2[K2, +V2] extends PartialFunction[K2, V2]

trait Gen[L[_]] { def make: L[Unit] }

trait Functor[M[_]]:
def map[A, B](ma: M[A])(f: A => B): M[B]
object Functor:
implicit def inst1[K3]: Functor[[V3] =>> ExMap1[K3, V3]] = ???
implicit def inst2[K4]: Functor[[V4] =>> ExMap2[K4, V4]] = ???

class Test:
def foo(x: Unit): String = x.toString()
def foo[F[_]](using F: Functor[F], G: Gen[F]): F[String] =
val res1: F[String] = F.map[Unit, String](G.make)(foo) // was: error
val res2: F[String] = F.map[Unit, String](G.make)(foo(_)) // was: ok

??? : F[String]

0 comments on commit 6331820

Please sign in to comment.