Skip to content

Commit be375ec

Browse files
authored
Improvements for implicit searches with top-level type variables (#16001)
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
2 parents 2bff208 + 741ebf2 commit be375ec

File tree

12 files changed

+144
-14
lines changed

12 files changed

+144
-14
lines changed

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

+7-4
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,25 @@ object ErrorReporting {
125125
def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = {
126126
val normTp = normalize(tree.tpe, pt)
127127
val normPt = normalize(pt, pt)
128-
128+
129129
def contextFunctionCount(tp: Type): Int = tp.stripped match
130130
case defn.ContextFunctionType(_, restp, _) => 1 + contextFunctionCount(restp)
131131
case _ => 0
132132
def strippedTpCount = contextFunctionCount(tree.tpe) - contextFunctionCount(normTp)
133133
def strippedPtCount = contextFunctionCount(pt) - contextFunctionCount(normPt)
134-
134+
135135
val (treeTp, expectedTp) =
136136
if normTp <:< normPt || strippedTpCount != strippedPtCount
137137
then (tree.tpe, pt)
138138
else (normTp, normPt)
139139
// use normalized types if that also shows an error, and both sides stripped
140140
// the same number of context functions. Use original types otherwise.
141-
141+
142142
def missingElse = tree match
143143
case If(_, _, elsep @ Literal(Constant(()))) if elsep.span.isSynthetic =>
144144
"\nMaybe you are missing an else part for the conditional?"
145145
case _ => ""
146-
146+
147147
errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse))
148148
}
149149

@@ -262,6 +262,9 @@ class ImplicitSearchError(
262262
case _ =>
263263
defaultAmbiguousImplicitMsg(ambi)
264264
}
265+
case ambi @ TooUnspecific(target) =>
266+
ex"""No implicit search was attempted${location("for")}
267+
|since the expected type $target is not specific enough"""
265268
case _ =>
266269
val shortMessage = userDefinedImplicitNotFoundParamMessage
267270
.orElse(userDefinedImplicitNotFoundTypeMessage)

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

+7-4
Original file line numberDiff line numberDiff line change
@@ -411,14 +411,14 @@ object Implicits:
411411

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

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

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

514517
/** An ambiguous implicits failure */
@@ -1484,7 +1487,7 @@ trait Implicits:
14841487

14851488
private def searchImplicit(contextual: Boolean): SearchResult =
14861489
if isUnderspecified(wildProto) then
1487-
NoMatchingImplicitsFailure
1490+
SearchFailure(TooUnspecific(pt), span)
14881491
else
14891492
val eligible =
14901493
if contextual then

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -822,17 +822,23 @@ object ProtoTypes {
822822
/** Approximate occurrences of parameter types and uninstantiated typevars
823823
* by wildcard types.
824824
*/
825-
private def wildApprox(tp: Type, theMap: WildApproxMap | Null, seen: Set[TypeParamRef], internal: Set[TypeLambda])(using Context): Type = tp match {
825+
private def wildApprox(tp: Type, theMap: WildApproxMap | Null, seen: Set[TypeParamRef], internal: Set[TypeLambda])(using Context): Type =
826+
tp match {
826827
case tp: NamedType => // default case, inlined for speed
827828
val isPatternBoundTypeRef = tp.isInstanceOf[TypeRef] && tp.symbol.isPatternBound
828829
if (isPatternBoundTypeRef) WildcardType(tp.underlying.bounds)
829830
else if (tp.symbol.isStatic || (tp.prefix `eq` NoPrefix)) tp
830831
else tp.derivedSelect(wildApprox(tp.prefix, theMap, seen, internal))
831832
case tp @ AppliedType(tycon, args) =>
833+
def wildArgs = args.mapConserve(arg => wildApprox(arg, theMap, seen, internal))
832834
wildApprox(tycon, theMap, seen, internal) match {
833-
case _: WildcardType => WildcardType // this ensures we get a * type
834-
case tycon1 => tp.derivedAppliedType(tycon1,
835-
args.mapConserve(arg => wildApprox(arg, theMap, seen, internal)))
835+
case WildcardType(TypeBounds(lo, hi)) if hi.typeParams.hasSameLengthAs(args) =>
836+
val args1 = wildArgs
837+
val lo1 = if lo.typeParams.hasSameLengthAs(args) then lo.appliedTo(args1) else lo
838+
WildcardType(TypeBounds(lo1, hi.appliedTo(args1)))
839+
case WildcardType(_) =>
840+
WildcardType
841+
case tycon1 => tp.derivedAppliedType(tycon1, wildArgs)
836842
}
837843
case tp: RefinedType => // default case, inlined for speed
838844
tp.derivedRefinedType(

tests/neg/i15998.check

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i15998.scala:6:12 -------------------------------------------------------------
2+
6 |val _ = foo(1) // error
3+
| ^
4+
| Found: (1 : Int)
5+
| Required: CC[A]
6+
|
7+
| where: A is a type variable
8+
| CC is a type variable with constraint <: [B] =>> Any
9+
|
10+
| Note that implicit conversions were not tried because the result of an implicit conversion
11+
| must be more specific than CC[A]
12+
|
13+
| longer explanation available when compiling with `-explain`
14+
-- Error: tests/neg/i15998.scala:11:11 ---------------------------------------------------------------------------------
15+
11 |val _ = bar // error
16+
| ^
17+
| No implicit search was attempted for parameter x of method bar
18+
| since the expected type X is not specific enough
19+
|
20+
| where: X is a type variable

tests/neg/i15998.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
given split: Conversion[Int, List[Int]] = ???
3+
4+
def foo[A, CC[B]](ring: CC[A]): Unit = ()
5+
6+
val _ = foo(1) // error
7+
8+
9+
def bar[X](using x: X): X = x
10+
11+
val _ = bar // error

tests/pos/i15160.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
trait Eq[A] {
2+
def eqv(a1: A, a2: A): Boolean
3+
}
4+
5+
given stringEq: Eq[String] with {
6+
def eqv(a1: String, a2: String) = a1 == a2
7+
}
8+
9+
abstract class Newtype[Src] {
10+
opaque type Type = Src
11+
12+
protected final def derive[F[_]](using ev: F[Src]): F[Type] = ev
13+
}
14+
15+
object Sample extends Newtype[String] {
16+
given eq: Eq[Type] = derive
17+
}

tests/pos/i15670.scala

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
trait JsonRowEntry {
2+
def readAs[E](implicit c: Read[E]): Option[E] = ???
3+
}
4+
trait Read[T]
5+
trait Codec[T] extends Read[T]
6+
trait CodecTypeProjection[C[_]]
7+
object JsonTransform {
8+
given SetCodec[T, C[_]: CodecTypeProjection]: scala.Conversion[C[T], C[Set[T]]] = ???
9+
given SetCodecExp[T, C[_]: CodecTypeProjection](using codec: C[T]): C[Set[T]] = codec
10+
given Codec[String] = ???
11+
given CodecTypeProjection[Read] = ???
12+
}
13+
14+
@main def Test() = {
15+
import JsonTransform.given
16+
val tree = new JsonRowEntry {}
17+
tree.readAs[Set[String]]
18+
}
19+
20+
trait Box[E]
21+
22+
trait Domain
23+
24+
def fun[E, D[_] <: Domain](box: Box[E])(implicit domain: D[E]): Unit = {
25+
26+
val newBox: Box[E] = ???
27+
28+
fun(newBox)
29+
}

tests/pos/i15820.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sealed trait Domain[E]
2+
3+
final def splitBounds[E, D[X] <: Domain[X]](
4+
bounds: Seq[E],
5+
)( using domain: D[E]): Seq[E] =
6+
val newBounds: Seq[E] = ???
7+
splitBounds(newBounds) // does not compile
8+
splitBounds[E,D](newBounds) // does compile

tests/run/i13986.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mu

tests/run/i13986.scala

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
sealed trait Xa[T]
2+
sealed trait Mu[T] extends Xa[T]
3+
object Xa {
4+
implicit def convertMu[X[x] <: Xa[x], A, B](implicit t: X[A]): X[B] = t.asInstanceOf[X[B]]
5+
}
6+
object Mu {
7+
implicit def mu: Mu[Int] = new Mu[Int] {
8+
override def toString = "mu"
9+
}
10+
}
11+
12+
object Test extends App {
13+
def constrain(a: Mu[Long]): Unit = println(a)
14+
constrain(Xa.convertMu)
15+
}

tests/neg/i13987.scala renamed to tests/run/i13987.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ object Mu {
1010
implicit def mu: Mu[Int] = new Mu[Int] {}
1111
}
1212

13-
object App extends App {
14-
def constrain(a: Mu[Long]): Unit = println(a)
13+
object Test extends App {
14+
def constrain(a: Mu[Long]): Unit = ()
1515
constrain(Xa.convertMu) // error
1616
}

tests/run/i15998.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import scala.collection.SeqOps
2+
3+
trait ComparingOps:
4+
extension[A, CC[B] <: SeqOps[B, CC, CC[B]]](ring: CC[A])
5+
def isRotationOf(that: CC[A]): Boolean = true
6+
7+
object RingSeq extends ComparingOps
8+
import RingSeq.*
9+
10+
@main def Test =
11+
RingSeq.isRotationOf("DAB") // error
12+
"ABCD".isRotationOf("DAB") // error
13+
14+
// workaround
15+
RingSeq.isRotationOf[Char, IndexedSeq]("DAB")
16+
RingSeq.isRotationOf(wrapString("DAB"))
17+
wrapString("ABCD").isRotationOf("DAB")

0 commit comments

Comments
 (0)