diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e8c05f7bc6c0..e7afbead1000 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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. */ @@ -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 _ => diff --git a/tests/neg/i16018.scala b/tests/neg/i16018.scala new file mode 100644 index 000000000000..4dbef902c9c8 --- /dev/null +++ b/tests/neg/i16018.scala @@ -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 diff --git a/tests/pos/i16018.scala b/tests/pos/i16018.scala new file mode 100644 index 000000000000..ae7523c17439 --- /dev/null +++ b/tests/pos/i16018.scala @@ -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() + ??? diff --git a/tests/pos/i16018b.scala b/tests/pos/i16018b.scala new file mode 100644 index 000000000000..826a6a190745 --- /dev/null +++ b/tests/pos/i16018b.scala @@ -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