Skip to content

Commit

Permalink
Don't capture wildcards if in closure or by-name (#16732)
Browse files Browse the repository at this point in the history
  • Loading branch information
odersky authored Jan 24, 2023
2 parents 43d0ec4 + 29dc069 commit 9f1d4b6
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 1 deletion.
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ object Inferencing {
case tp: AnnotatedType => tp.derivedAnnotatedType(captureWildcards(tp.parent), tp.annot)
case _ => tp
}

def hasCaptureConversionArg(tp: Type)(using Context): Boolean = tp match
case tp: AppliedType => tp.args.exists(_.typeSymbol == defn.TypeBox_CAP)
case _ => false
}

trait Inferencing { this: Typer =>
Expand Down
20 changes: 19 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import Decorators._
import Uniques._
import inlines.Inlines
import config.Printers.typr
import Inferencing.*
import ErrorReporting.*
import util.SourceFile
import TypeComparer.necessarySubType

Expand Down Expand Up @@ -492,7 +494,23 @@ object ProtoTypes {
val targ = cacheTypedArg(arg,
typer.typedUnadapted(_, wideFormal, locked)(using argCtx),
force = true)
typer.adapt(targ, wideFormal, locked)
val targ1 = typer.adapt(targ, wideFormal, locked)
if wideFormal eq formal then targ1
else checkNoWildcardCaptureForCBN(targ1)
}

def checkNoWildcardCaptureForCBN(targ1: Tree)(using Context): Tree = {
if hasCaptureConversionArg(targ1.tpe) then
stripCast(targ1).tpe match
case tp: AppliedType if tp.hasWildcardArg =>
errorTree(targ1,
em"""argument for by-name parameter is not a value
|and contains wildcard arguments: $tp
|
|Assign it to a val and pass that instead.
|""")
case _ => targ1
else targ1
}

/** The type of the argument `arg`, or `NoType` if `arg` has not been typed before
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
else if ((tree.tpt `eq` untpd.ContextualEmptyTree) && mt.paramNames.isEmpty)
// Note implicitness of function in target type since there are no method parameters that indicate it.
TypeTree(defn.FunctionOf(Nil, mt.resType, isContextual = true, isErased = false))
else if hasCaptureConversionArg(mt.resType) then
errorTree(tree,
em"""cannot turn method type $mt into closure
|because it has capture conversion skolem types""")
else
EmptyTree
}
Expand Down
24 changes: 24 additions & 0 deletions tests/neg/t9419.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
trait Magic[S]:
def init: S
def step(s: S): String

object IntMagic extends Magic[Int]:
def init = 0
def step(s: Int): String = (s - 1).toString

object StrMagic extends Magic[String]:
def init = "hi"
def step(s: String): String = s.reverse

object Main:
def onestep[T](m: () => Magic[T]): String = m().step(m().init)
def unostep[T](m: => Magic[T]): String = m.step(m.init)

val iter: Iterator[Magic[?]] = Iterator.tabulate(Int.MaxValue)(i => if i % 2 == 0 then IntMagic else StrMagic)

// was: class java.lang.String cannot be cast to class java.lang.Integer
def main(args: Array[String]): Unit =
onestep(() => iter.next()) // error
unostep(iter.next()) // error
val m = iter.next()
unostep(m) // ok, because m is a value
20 changes: 20 additions & 0 deletions tests/pos/t9419.jackson.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// from failure in the community project
// jackson-module-scala
// in ScalaAnnotationIntrospectorModule.scala:139:12

import scala.language.implicitConversions

trait EnrichedType[X]:
def value: X

trait ClassW extends EnrichedType[Class[_]]:
def extendsScalaClass = false

class Test:
implicit def mkClassW(c: => Class[_]): ClassW = new ClassW:
lazy val value = c

def test1(c1: Class[_]) = c1.extendsScalaClass // ok: c1 is a value
def test2(c2: Class[_]) = mkClassW(c2).extendsScalaClass // ok: c2 is a value
// c1 in test1 goes throw adapting to find the extension method and gains the wildcard capture cast then
// c2 in test2 goes straight to typedArg, as it's already an arg, so it never gets wildcard captured

0 comments on commit 9f1d4b6

Please sign in to comment.