Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,19 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
def paramBounds(tparam: Symbol): TypeBounds =
tparam.info.substApprox(tparams2.asInstanceOf[List[Symbol]], args2).bounds

/** Does `bound` refer to `tparam`? Keeps the widening branches below off a
* recursive `paramBounds(tparam)`; `LazyRef` counts as recursive to avoid
* forcing class-header initialization.
*/
def hasRecursiveBound(bound: Type, tparam: Symbol): Boolean =
val acc = new TypeAccumulator[Boolean]:
def apply(x: Boolean, tp: Type): Boolean =
x || (tp match
case _: LazyRef => true
case tp: TypeRef => tp.symbol eq tparam
case _ => foldOver(x, tp))
acc(false, bound)

/** Test all arguments. Incomplete argument tests (according to isIncomplete) are deferred in
* the first run and picked up in the second.
*/
Expand Down Expand Up @@ -1890,10 +1903,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// The captured reference could be illegal and cause a
// TypeError to be thrown in argDenot
false
else if (v > 0)
isSubType(paramBounds(tparam).hi, arg2)
else if (v < 0)
isSubType(arg2, paramBounds(tparam).lo)
// Existential widening (#16018): for covariant `F`, `F[? <: hi] <: F[hi]`
// (dually contravariant). Use the wildcard's own bound intersected
// (resp. unioned) with the declared one; recursive declared bounds
// stay on the conservative path.
else if v > 0 then
if hasRecursiveBound(tparam.info.bounds.hi, tparam) then false
else isSubType(arg1.hi & paramBounds(tparam).hi, arg2)
else if v < 0 then
if hasRecursiveBound(tparam.info.bounds.lo, tparam) then false
else isSubType(arg2, arg1.lo | paramBounds(tparam).lo)
else
false
case _ =>
Expand Down
38 changes: 38 additions & 0 deletions tests/neg/i16018.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Fixes #16018: shapes that must stay rejected. The fix only widens
// covariant + upper-bounded and contravariant + lower-bounded wildcards;
// every other combination is unsound.
object Test:

class Co[+T]
class Contra[-T]
class Inv[T]
trait Holder

// invariant never widens
def inv_upper[M](xs: Inv[? <: M]): Inv[M] = xs // error
def inv_lower[M](xs: Inv[? >: M]): Inv[M] = xs // error

// wrong-direction wildcards do not widen
def co_lower[M](xs: Co[? >: M]): Co[M] = xs // error
def contra_upper[M](xs: Contra[? <: M]): Contra[M] = xs // error

// wildcard hi is wider than the target
def co_too_wide(xs: Co[? <: Any]): Co[Holder] = xs // error

// boundary: the effective bound admits Holder, not a strict subtype
class Sub extends Holder
class CoBounded[+T <: Holder]
def co_boundary(xs: CoBounded[? <: Any]): CoBounded[Sub] = xs // error
class Super
class HolderS extends Super
class ContraBounded[-T >: HolderS]
def contra_boundary(xs: ContraBounded[? >: Nothing]): ContraBounded[Super] = xs // error

// F-bounded (recursive) parameter stays on the conservative path
class FBounded[+T <: FBounded[T]]
def f_bounded[M <: FBounded[M]](xs: Co[FBounded[? <: M]]): Co[FBounded[M]] = xs // error

// per-position variance is still checked through HKT nesting
def fn_contra_upper[M, R](xs: Co[Function1[? <: M, R]]): Co[Function1[M, R]] = xs // error
def fn_co_lower[M, T1](xs: Co[Function1[T1, ? >: M]]): Co[Function1[T1, M]] = xs // error
def inv_in_co[M](xs: Co[Inv[? <: M]]): Co[Inv[M]] = xs // error
47 changes: 47 additions & 0 deletions tests/pos/i16018.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Fixes #16018: existential widening for wildcard arguments.
// Real-world reproductions: mbovel's minimization, a pure-Scala collection
// (no capture conversion), and the original akka report (a Java-generic
// argument re-projected as a Scala collection and `collect`-ed).
import scala.collection.immutable

object Test:

class Box[T](val value: T)
class Container[+S, +M]
class SubContainer[+S, +M] extends Container[S, M]

def f1[T](l: List[Box[? <: T]]): List[T] = l.map(_.value)

def f2[M](xs: immutable.Seq[Container[Any, ? <: M]]): immutable.Seq[Container[Any, M]] =
xs.collect {
case g: SubContainer[Any, M] @unchecked => g
case other => other
}

def seqOf[T](it: java.lang.Iterable[T]): immutable.Seq[T] = ???
def f3[M](xs: java.util.List[? <: Container[Any, ? <: M]]): immutable.Seq[Container[Any, M]] =
seqOf(xs).collect {
case g: SubContainer[Any, M] @unchecked => g
case other => other
}

// The exact akka-minimized repro from the ticket (modulo `_` -> `?`).
object Main:

class Source[+Out, +Mat] extends Graph[SourceShape[Out], Mat]
class Shape
class SourceShape[+T] extends Shape
class Graph[+S <: Shape, +M]

def immutableSeq[T](iterable: java.lang.Iterable[T]): immutable.Seq[T] = ???

def combine[T, U, M](sources: java.util.List[? <: Graph[SourceShape[T], ? <: M]])
: Source[U, java.util.List[M]] =
val seq: immutable.Seq[Graph[SourceShape[T], M]] =
if sources != null then
immutableSeq(sources).collect {
case source: Source[T, M] @unchecked => source
case other => other
}
else immutable.Seq()
???
32 changes: 32 additions & 0 deletions tests/pos/i16018b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Fixes #16018: the existential-widening directions the fix legitimizes.
// Covariant `F[? <: hi] <: F[hi]` and contravariant `G[? >: lo] <: G[lo]`,
// the declared-bound intersection/union guard, and the same through HKT.
object Test:

class Co[+T]
class Contra[-T]
trait Holder

def co_upper[M](xs: Co[? <: M]): Co[M] = xs
def contra_lower[M](xs: Contra[? >: M]): Contra[M] = xs

// wildcard hi (Any) intersected with the declared bound: Any & Holder = Holder
class CoBounded[+T <: Holder]
def co_declared_bound(xs: CoBounded[? <: Any]): CoBounded[Holder] = xs
// dual: wildcard lo (Nothing) unioned with the declared lower bound
class ContraBounded[-T >: Holder]
def contra_declared_bound(xs: ContraBounded[? >: Nothing]): ContraBounded[Holder] = xs

// mixed variance through HKT nesting
def fn_in_co[M, R](xs: Co[Function1[? >: M, R]]): Co[Function1[M, R]] = xs

// intersection and path-dependent upper bounds compose with the fix
trait A; trait B
def inter_upper(xs: Co[? <: A & B]): Co[A] = xs
trait Outer { type T }
def path_dep(o: Outer)(xs: Co[? <: o.T]): Co[o.T] = xs

// the recursive-bound guard rejects only self-referential bounds; an
// ordinary dependent bound still widens
class DepBound[+T <: U, U]
def acyclic_dependent[M](xs: DepBound[? <: M, M]): DepBound[M, M] = xs
Loading