diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index cd33fe9cef24..224359faaf3b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -714,8 +714,8 @@ trait Applications extends Compatibility { || argMatch == ArgMatch.CompatibleCAP && { val argtpe1 = argtpe.widen - val captured = captureWildcards(argtpe1) - (captured ne argtpe1) && isCompatible(captured, formal.widenExpr) + val captured = captureWildcardsCompat(argtpe1, formal.widenExpr) + captured ne argtpe1 } /** The type of the given argument */ @@ -2412,4 +2412,9 @@ trait Applications extends Compatibility { def isApplicableExtensionMethod(methodRef: TermRef, receiverType: Type)(using Context): Boolean = methodRef.symbol.is(ExtensionMethod) && !receiverType.isBottomType && tryApplyingExtensionMethod(methodRef, nullLiteral.asInstance(receiverType)).nonEmpty + + def captureWildcardsCompat(tp: Type, pt: Type)(using Context): Type = + val captured = captureWildcards(tp) + if (captured ne tp) && isCompatible(captured, pt) then captured + else tp } diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 2aef3433228b..bd2f365cbba3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -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 => diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 8ba842ad695f..ef9b083bef4f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -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 @@ -492,7 +494,21 @@ 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 + val tp = stripCast(targ1).tpe + 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. + |""") + else targ1 } /** The type of the argument `arg`, or `NoType` if `arg` has not been typed before diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1a24a94e527e..425b800a0e72 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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 } @@ -3940,7 +3944,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer return adaptConstant(tree, ConstantType(converted)) case _ => - val captured = captureWildcards(wtp) + val captured = captureWildcardsCompat(wtp, pt) if (captured `ne` wtp) return readapt(tree.cast(captured)) diff --git a/tests/neg/t9419.scala b/tests/neg/t9419.scala new file mode 100644 index 000000000000..e9358c0ba641 --- /dev/null +++ b/tests/neg/t9419.scala @@ -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 diff --git a/tests/neg/t9419.zio-http.scala b/tests/neg/t9419.zio-http.scala new file mode 100644 index 000000000000..cff9ec51e6f9 --- /dev/null +++ b/tests/neg/t9419.zio-http.scala @@ -0,0 +1,18 @@ +// Minimisation of how the fix for t9419 affected zio-http +import java.util.concurrent.Future as JFuture + +trait Test: + def shutdownGracefully(): JFuture[_] + + def executedWildcard(jFuture: => JFuture[_]): Unit + def executedGeneric[A](jFuture: => JFuture[A]): Unit + def executedWildGen[A](jFuture: => JFuture[? <: A]): Unit + + // Even though JFuture is morally covariant, at least currently, + // there's no definition-side variance, so it's treated as invariant. + // So we have to be concerned that two different values of `JFuture[A]` + // with different types, blowing up together. So error in `fails`. + def works = executedWildcard(shutdownGracefully()) + def fails = executedGeneric(shutdownGracefully()) // error + def fixed = executedGeneric(shutdownGracefully().asInstanceOf[JFuture[Any]]) // fix + def best2 = executedWildGen(shutdownGracefully()) // even better, use use-site variance in the method diff --git a/tests/pos/t9419.jackson.scala b/tests/pos/t9419.jackson.scala new file mode 100644 index 000000000000..bf26c7e4c672 --- /dev/null +++ b/tests/pos/t9419.jackson.scala @@ -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 diff --git a/tests/pos/t9419.specs2.scala b/tests/pos/t9419.specs2.scala new file mode 100644 index 000000000000..fe4a44312594 --- /dev/null +++ b/tests/pos/t9419.specs2.scala @@ -0,0 +1,13 @@ +// Minimisation of how the fix for t9419 affected specs2 +class MustExpectable[T](tm: () => T): + def must_==(other: => Any) = tm() == other + +class Foo + +object Main: + implicit def theValue[T](t: => T): MustExpectable[T] = new MustExpectable(() => t) + def main(args: Array[String]): Unit = + val cls = classOf[Foo] + val instance = new Foo() + val works = cls must_== cls + val fails = instance.getClass must_== cls