Skip to content

Commit 1c0e383

Browse files
committed
Restrict captureWildcards to only be used if needed
Rather than blindly using the newly wildcard-captured type, check that it's compatible with the proto/formal type. That way values that have wildcard types can be passed, uncast, to extension methods that don't require the capture. For instance in specs2, a value of type `Class[? <: Foo]` needn't become `Class[?1.CAP]` just so it can be applied to `def theValue[T](t: => T)`. For the zio-http case, despite knowing that JFuture is morally covariant, we don't have any way to knowing that - so we must be safe and error.
1 parent b8ad7b1 commit 1c0e383

File tree

6 files changed

+46
-12
lines changed

6 files changed

+46
-12
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,8 @@ trait Applications extends Compatibility {
714714
|| argMatch == ArgMatch.CompatibleCAP
715715
&& {
716716
val argtpe1 = argtpe.widen
717-
val captured = captureWildcards(argtpe1)
718-
(captured ne argtpe1) && isCompatible(captured, formal.widenExpr)
717+
val captured = captureWildcardsCompat(argtpe1, formal.widenExpr, self)
718+
captured ne argtpe1
719719
}
720720

721721
/** The type of the given argument */

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,11 @@ object Inferencing {
543543
case _ => tp
544544
}
545545

546+
def captureWildcardsCompat(tp: Type, pt: Type, compat: Compatibility)(using Context): Type =
547+
val captured = captureWildcards(tp)
548+
if (captured ne tp) && compat.isCompatible(captured, pt) then captured
549+
else tp
550+
546551
def hasCaptureConversionArg(tp: Type)(using Context): Boolean = tp match
547552
case tp: AppliedType => tp.args.exists(_.typeSymbol == defn.TypeBox_CAP)
548553
case _ => false

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -501,15 +501,13 @@ object ProtoTypes {
501501

502502
def checkNoWildcardCaptureForCBN(targ1: Tree)(using Context): Tree = {
503503
if hasCaptureConversionArg(targ1.tpe) then
504-
stripCast(targ1).tpe match
505-
case tp: AppliedType if tp.hasWildcardArg =>
506-
errorTree(targ1,
507-
em"""argument for by-name parameter is not a value
508-
|and contains wildcard arguments: $tp
509-
|
510-
|Assign it to a val and pass that instead.
511-
|""")
512-
case _ => targ1
504+
val tp = stripCast(targ1).tpe
505+
errorTree(targ1,
506+
em"""argument for by-name parameter is not a value
507+
|and contains wildcard arguments: $tp
508+
|
509+
|Assign it to a val and pass that instead.
510+
|""")
513511
else targ1
514512
}
515513

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3962,7 +3962,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
39623962
return adaptConstant(tree, ConstantType(converted))
39633963
case _ =>
39643964

3965-
val captured = captureWildcards(wtp)
3965+
val captured = captureWildcardsCompat(wtp, pt, this)
39663966
if (captured `ne` wtp)
39673967
return readapt(tree.cast(captured))
39683968

tests/neg/t9419.zio-http.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Minimisation of how the fix for t9419 affected zio-http
2+
import java.util.concurrent.Future as JFuture
3+
4+
trait Test:
5+
def shutdownGracefully(): JFuture[_]
6+
7+
def executedWildcard(jFuture: => JFuture[_]): Unit
8+
def executedGeneric[A](jFuture: => JFuture[A]): Unit
9+
def executedWildGen[A](jFuture: => JFuture[? <: A]): Unit
10+
11+
// Even though JFuture is morally covariant, at least currently,
12+
// there's no definition-side variance, so it's treated as invariant.
13+
// So we have to be concerned that two different values of `JFuture[A]`
14+
// with different types, blowing up together. So error in `fails`.
15+
def works = executedWildcard(shutdownGracefully())
16+
def fails = executedGeneric(shutdownGracefully()) // error
17+
def fixed = executedGeneric(shutdownGracefully().asInstanceOf[JFuture[Any]]) // fix
18+
def best2 = executedWildGen(shutdownGracefully()) // even better, use use-site variance in the method

tests/pos/t9419.specs2.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Minimisation of how the fix for t9419 affected specs2
2+
class MustExpectable[T](tm: () => T):
3+
def must_==(other: => Any) = tm() == other
4+
5+
class Foo
6+
7+
object Main:
8+
implicit def theValue[T](t: => T): MustExpectable[T] = new MustExpectable(() => t)
9+
def main(args: Array[String]): Unit =
10+
val cls = classOf[Foo]
11+
val instance = new Foo()
12+
val works = cls must_== cls
13+
val fails = instance.getClass must_== cls

0 commit comments

Comments
 (0)