Skip to content

Commit

Permalink
Improvements for implicit searches with top-level type variables (#16001
Browse files Browse the repository at this point in the history
)

Two improvements for implicit searches involving type variables.

1. We now always add a comment when an implicit search is rejected due
to the "too unspecific" criterion of #13886, commit
[Refine checking for underspecified implicit
queries](db5956b).

There have been quite a few regressions that hit that problem, so it is
good to know immediately what
   the issue is. 

2. There is now a better wildcard approximation of higher-kinded type
applications. This makes several programs (including original #15998)
compile, which were classified as not specific enough before.

Fixes #15998
Fixes #15820
Fixes #15670
Fixes #15160 
Fixes #13986
  • Loading branch information
smarter committed Sep 9, 2022
2 parents 2bff208 + 741ebf2 commit be375ec
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 14 deletions.
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Expand Up @@ -125,25 +125,25 @@ object ErrorReporting {
def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = {
val normTp = normalize(tree.tpe, pt)
val normPt = normalize(pt, pt)

def contextFunctionCount(tp: Type): Int = tp.stripped match
case defn.ContextFunctionType(_, restp, _) => 1 + contextFunctionCount(restp)
case _ => 0
def strippedTpCount = contextFunctionCount(tree.tpe) - contextFunctionCount(normTp)
def strippedPtCount = contextFunctionCount(pt) - contextFunctionCount(normPt)

val (treeTp, expectedTp) =
if normTp <:< normPt || strippedTpCount != strippedPtCount
then (tree.tpe, pt)
else (normTp, normPt)
// use normalized types if that also shows an error, and both sides stripped
// the same number of context functions. Use original types otherwise.

def missingElse = tree match
case If(_, _, elsep @ Literal(Constant(()))) if elsep.span.isSynthetic =>
"\nMaybe you are missing an else part for the conditional?"
case _ => ""

errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse))
}

Expand Down Expand Up @@ -262,6 +262,9 @@ class ImplicitSearchError(
case _ =>
defaultAmbiguousImplicitMsg(ambi)
}
case ambi @ TooUnspecific(target) =>
ex"""No implicit search was attempted${location("for")}
|since the expected type $target is not specific enough"""
case _ =>
val shortMessage = userDefinedImplicitNotFoundParamMessage
.orElse(userDefinedImplicitNotFoundTypeMessage)
Expand Down
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Expand Up @@ -411,14 +411,14 @@ object Implicits:

/** A failed search */
case class SearchFailure(tree: Tree) extends SearchResult {
final def isAmbiguous: Boolean = tree.tpe.isInstanceOf[AmbiguousImplicits]
final def isAmbiguous: Boolean = tree.tpe.isInstanceOf[AmbiguousImplicits | TooUnspecific]
final def reason: SearchFailureType = tree.tpe.asInstanceOf[SearchFailureType]
}

object SearchFailure {
def apply(tpe: SearchFailureType, span: Span)(using Context): SearchFailure = {
val id = tpe match
case tpe: AmbiguousImplicits =>
case tpe: (AmbiguousImplicits | TooUnspecific) =>
untpd.SearchFailureIdent(nme.AMBIGUOUS, s"/* ambiguous: ${tpe.explanation} */")
case _ =>
untpd.SearchFailureIdent(nme.MISSING, "/* missing */")
Expand Down Expand Up @@ -504,11 +504,14 @@ object Implicits:
SearchFailure(ImplicitSearchTooLarge, NoSpan)(using NoContext)

/** A failure value indicating that an implicit search for a conversion was not tried */
class TooUnspecific(target: Type) extends NoMatchingImplicits(NoType, EmptyTree, OrderingConstraint.empty):
case class TooUnspecific(target: Type) extends NoMatchingImplicits(NoType, EmptyTree, OrderingConstraint.empty):
override def whyNoConversion(using Context): String =
i"""
|Note that implicit conversions were not tried because the result of an implicit conversion
|must be more specific than $target"""
override def explanation(using Context) =
i"""${super.explanation}.
|The expected type $target is not specific enough, so no search was attempted"""
override def toString = s"TooUnspecific"

/** An ambiguous implicits failure */
Expand Down Expand Up @@ -1484,7 +1487,7 @@ trait Implicits:

private def searchImplicit(contextual: Boolean): SearchResult =
if isUnderspecified(wildProto) then
NoMatchingImplicitsFailure
SearchFailure(TooUnspecific(pt), span)
else
val eligible =
if contextual then
Expand Down
14 changes: 10 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Expand Up @@ -822,17 +822,23 @@ object ProtoTypes {
/** Approximate occurrences of parameter types and uninstantiated typevars
* by wildcard types.
*/
private def wildApprox(tp: Type, theMap: WildApproxMap | Null, seen: Set[TypeParamRef], internal: Set[TypeLambda])(using Context): Type = tp match {
private def wildApprox(tp: Type, theMap: WildApproxMap | Null, seen: Set[TypeParamRef], internal: Set[TypeLambda])(using Context): Type =
tp match {
case tp: NamedType => // default case, inlined for speed
val isPatternBoundTypeRef = tp.isInstanceOf[TypeRef] && tp.symbol.isPatternBound
if (isPatternBoundTypeRef) WildcardType(tp.underlying.bounds)
else if (tp.symbol.isStatic || (tp.prefix `eq` NoPrefix)) tp
else tp.derivedSelect(wildApprox(tp.prefix, theMap, seen, internal))
case tp @ AppliedType(tycon, args) =>
def wildArgs = args.mapConserve(arg => wildApprox(arg, theMap, seen, internal))
wildApprox(tycon, theMap, seen, internal) match {
case _: WildcardType => WildcardType // this ensures we get a * type
case tycon1 => tp.derivedAppliedType(tycon1,
args.mapConserve(arg => wildApprox(arg, theMap, seen, internal)))
case WildcardType(TypeBounds(lo, hi)) if hi.typeParams.hasSameLengthAs(args) =>
val args1 = wildArgs
val lo1 = if lo.typeParams.hasSameLengthAs(args) then lo.appliedTo(args1) else lo
WildcardType(TypeBounds(lo1, hi.appliedTo(args1)))
case WildcardType(_) =>
WildcardType
case tycon1 => tp.derivedAppliedType(tycon1, wildArgs)
}
case tp: RefinedType => // default case, inlined for speed
tp.derivedRefinedType(
Expand Down
20 changes: 20 additions & 0 deletions tests/neg/i15998.check
@@ -0,0 +1,20 @@
-- [E007] Type Mismatch Error: tests/neg/i15998.scala:6:12 -------------------------------------------------------------
6 |val _ = foo(1) // error
| ^
| Found: (1 : Int)
| Required: CC[A]
|
| where: A is a type variable
| CC is a type variable with constraint <: [B] =>> Any
|
| Note that implicit conversions were not tried because the result of an implicit conversion
| must be more specific than CC[A]
|
| longer explanation available when compiling with `-explain`
-- Error: tests/neg/i15998.scala:11:11 ---------------------------------------------------------------------------------
11 |val _ = bar // error
| ^
| No implicit search was attempted for parameter x of method bar
| since the expected type X is not specific enough
|
| where: X is a type variable
11 changes: 11 additions & 0 deletions tests/neg/i15998.scala
@@ -0,0 +1,11 @@

given split: Conversion[Int, List[Int]] = ???

def foo[A, CC[B]](ring: CC[A]): Unit = ()

val _ = foo(1) // error


def bar[X](using x: X): X = x

val _ = bar // error
17 changes: 17 additions & 0 deletions tests/pos/i15160.scala
@@ -0,0 +1,17 @@
trait Eq[A] {
def eqv(a1: A, a2: A): Boolean
}

given stringEq: Eq[String] with {
def eqv(a1: String, a2: String) = a1 == a2
}

abstract class Newtype[Src] {
opaque type Type = Src

protected final def derive[F[_]](using ev: F[Src]): F[Type] = ev
}

object Sample extends Newtype[String] {
given eq: Eq[Type] = derive
}
29 changes: 29 additions & 0 deletions tests/pos/i15670.scala
@@ -0,0 +1,29 @@
trait JsonRowEntry {
def readAs[E](implicit c: Read[E]): Option[E] = ???
}
trait Read[T]
trait Codec[T] extends Read[T]
trait CodecTypeProjection[C[_]]
object JsonTransform {
given SetCodec[T, C[_]: CodecTypeProjection]: scala.Conversion[C[T], C[Set[T]]] = ???
given SetCodecExp[T, C[_]: CodecTypeProjection](using codec: C[T]): C[Set[T]] = codec
given Codec[String] = ???
given CodecTypeProjection[Read] = ???
}

@main def Test() = {
import JsonTransform.given
val tree = new JsonRowEntry {}
tree.readAs[Set[String]]
}

trait Box[E]

trait Domain

def fun[E, D[_] <: Domain](box: Box[E])(implicit domain: D[E]): Unit = {

val newBox: Box[E] = ???

fun(newBox)
}
8 changes: 8 additions & 0 deletions tests/pos/i15820.scala
@@ -0,0 +1,8 @@
sealed trait Domain[E]

final def splitBounds[E, D[X] <: Domain[X]](
bounds: Seq[E],
)( using domain: D[E]): Seq[E] =
val newBounds: Seq[E] = ???
splitBounds(newBounds) // does not compile
splitBounds[E,D](newBounds) // does compile
1 change: 1 addition & 0 deletions tests/run/i13986.check
@@ -0,0 +1 @@
mu
15 changes: 15 additions & 0 deletions tests/run/i13986.scala
@@ -0,0 +1,15 @@
sealed trait Xa[T]
sealed trait Mu[T] extends Xa[T]
object Xa {
implicit def convertMu[X[x] <: Xa[x], A, B](implicit t: X[A]): X[B] = t.asInstanceOf[X[B]]
}
object Mu {
implicit def mu: Mu[Int] = new Mu[Int] {
override def toString = "mu"
}
}

object Test extends App {
def constrain(a: Mu[Long]): Unit = println(a)
constrain(Xa.convertMu)
}
4 changes: 2 additions & 2 deletions tests/neg/i13987.scala → tests/run/i13987.scala
Expand Up @@ -10,7 +10,7 @@ object Mu {
implicit def mu: Mu[Int] = new Mu[Int] {}
}

object App extends App {
def constrain(a: Mu[Long]): Unit = println(a)
object Test extends App {
def constrain(a: Mu[Long]): Unit = ()
constrain(Xa.convertMu) // error
}
17 changes: 17 additions & 0 deletions tests/run/i15998.scala
@@ -0,0 +1,17 @@
import scala.collection.SeqOps

trait ComparingOps:
extension[A, CC[B] <: SeqOps[B, CC, CC[B]]](ring: CC[A])
def isRotationOf(that: CC[A]): Boolean = true

object RingSeq extends ComparingOps
import RingSeq.*

@main def Test =
RingSeq.isRotationOf("DAB") // error
"ABCD".isRotationOf("DAB") // error

// workaround
RingSeq.isRotationOf[Char, IndexedSeq]("DAB")
RingSeq.isRotationOf(wrapString("DAB"))
wrapString("ABCD").isRotationOf("DAB")

0 comments on commit be375ec

Please sign in to comment.