Skip to content

Commit b952dfd

Browse files
committed
Fix #16018: Existential widening for wildcard arguments
The Scala 3 typer was rejecting subtypings of the form F[? >: lo <: hi] <: F[X] for covariant or contravariant `F`, even when `X = hi` (covariant) or `X = lo` (contravariant). This is a regression against Scala 2 and surfaced as the akka-derived report in #16018: def f[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]]) : Seq[Container[Any, M]] = seqOf(xs).collect { case g: SubContainer[Any, M] @unchecked => g case other => other } Once `Inferencing.captureWildcards` lifted the Java wildcard to a `TypeBox.CAP` skolem and pattern matching narrowed the first case body to `pat ∩ ?N.CAP`, the inferred result reduced to a chain of subtype checks that ultimately required `F[? <: M] <:< F[M]` for covariant `F`. That check went through `TypeComparer.compareCaptured` and was answered in the negative. Type-theoretic justification ---------------------------- For a covariant type constructor `F` and an existential `∃X. X <: hi`, ⨆{F[X] | X <: hi} = F[hi] (covariant supremum) — the supremum is attained at `X = hi`, by covariance. The compiler must admit that supremum as a subtype of `F[hi]`. Dually for contravariance with the lower bound: ⨅{G[X] | X >: lo} = G[lo] (contravariant infimum) This is the standard existential-elimination rule for variant occurrences. Bug --- `compareCaptured` was checking v > 0: isSubType(paramBounds(tparam).hi, arg2) v < 0: isSubType(arg2, paramBounds(tparam).lo) i.e. it asked whether the *declared parameter bound* (which collapses to `Any` / `Nothing` for unconstrained type parameters) conforms to `arg2`, instead of asking whether the *wildcard's own bound* (`arg1.hi` / `arg1.lo`) does. That rejected every interesting case. Fix --- In `TypeComparer.compareCaptured` use the wildcard's own hi/lo, intersected (resp. unioned) with the declared parameter bounds to preserve soundness in the corner case where a wildcard is wider than its parameter permits: v > 0: isSubType(arg1.hi & paramBounds(tparam).hi, arg2) v < 0: isSubType(arg2, arg1.lo | paramBounds(tparam).lo) Tests (directional matrix) -------------------------- - `tests/pos/i16018.scala` — mbovel's minimization plus a pure-Scala matrix that exercises subtype patterns + fallback `case other` over wildcards. - `tests/pos/i16018b.scala` — the akka-style shapes that motivated the ticket: `java.lang.Iterable[? <: G[? <: M]]` / `java.util.List[…]` routed through `seqOf`, then `collect` / `map` with subtype + fallback patterns. - `tests/pos/i16018c.scala` — positive half of the variance × bound matrix for the fix itself: covariant + upper, contravariant + lower, nested, via method type params, with parameter-bound tightening. - `tests/neg/i16018c.scala` — negative half of the matrix: invariant containers, covariant + lower, contravariant + upper, and the soundness guard against wildcards wider than the required type. These must remain rejected after the fix. Verified locally ---------------- - All four `i16018*` tests: 16/16 pass via `sbt 'scala3-compiler-bootstrapped/testOnly … CompilationTests'` with `-Ddotty.tests.filter=i16018`. - `wildcard` / `match` / `variance` filter subsets: 16/16 each. - Full `CompilationTests` run: the remaining 4 failures (`tests/run/i13358.scala`, `tests/run/lazy-*.scala`, `tests/run/t5552.scala`, `tests/run/t7406.scala`, `tests/run/isInstanceOf-eval.scala`, `tests/run/i24553.scala`, `tests/pos-custom-args/captures/fill-cbn.scala`, and the capture-checking neg suite) reproduce on `main` without this change and are caused by JDK 25 environment drift (`sun.misc.Unsafe` deprecation messages on stdout, the introduction of `java.lang.IO`, and removal of `native` from `Object.wait`).
1 parent b6e5747 commit b952dfd

7 files changed

Lines changed: 371 additions & 2 deletions

File tree

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,10 +1890,25 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
18901890
// The captured reference could be illegal and cause a
18911891
// TypeError to be thrown in argDenot
18921892
false
1893+
// Existential widening for wildcard arguments (issue #16018):
1894+
// `arg1` is the bounds carried by a `? >: lo <: hi` wildcard. For
1895+
// `arg2` to be a (covariant / contravariant) supertype of `arg1`,
1896+
// we only need the *wildcard's own* hi/lo to conform — the
1897+
// declared parameter bounds (`paramBounds(tparam)`) only ever
1898+
// *constrain* the wildcard further, never relax it. The earlier
1899+
// formulation used `paramBounds(tparam).hi`, which collapses to
1900+
// `Any` for unconstrained type parameters and rejects valid
1901+
// subtypings such as `F[? <: M] <: F[M]` for covariant `F`
1902+
// (mathematically: ⨆{F[X] | X <: M} = F[M] by covariance, since
1903+
// the supremum is attained at X = M). Intersecting with
1904+
// `paramBounds(tparam)` keeps soundness in the rare case where
1905+
// the wildcard's bound is *wider* than what `tparam` permits.
18931906
else if (v > 0)
1894-
isSubType(paramBounds(tparam).hi, arg2)
1907+
val effectiveHi = arg1.hi & paramBounds(tparam).hi
1908+
isSubType(effectiveHi, arg2)
18951909
else if (v < 0)
1896-
isSubType(arg2, paramBounds(tparam).lo)
1910+
val effectiveLo = arg1.lo | paramBounds(tparam).lo
1911+
isSubType(arg2, effectiveLo)
18971912
else
18981913
false
18991914
case _ =>

tests/neg/i16018c.check

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:18:50 -----------------------------------------------------------
2+
18 | def inv_upper_bad[M](xs: Inv[? <: M]): Inv[M] = xs // error
3+
| ^^
4+
| Found: (xs : Test.Inv[? <: M])
5+
| Required: Test.Inv[M]
6+
|
7+
| longer explanation available when compiling with `-explain`
8+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:19:50 -----------------------------------------------------------
9+
19 | def inv_lower_bad[M](xs: Inv[? >: M]): Inv[M] = xs // error
10+
| ^^
11+
| Found: (xs : Test.Inv[? >: M])
12+
| Required: Test.Inv[M]
13+
|
14+
| longer explanation available when compiling with `-explain`
15+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:26:47 -----------------------------------------------------------
16+
26 | def co_lower_bad[M](xs: Co[? >: M]): Co[M] = xs // error
17+
| ^^
18+
| Found: (xs : Test.Co[? >: M])
19+
| Required: Test.Co[M]
20+
|
21+
| longer explanation available when compiling with `-explain`
22+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:32:59 -----------------------------------------------------------
23+
32 | def contra_upper_bad[M](xs: Contra[? <: M]): Contra[M] = xs // error
24+
| ^^
25+
| Found: (xs : Test.Contra[? <: M])
26+
| Required: Test.Contra[M]
27+
|
28+
| longer explanation available when compiling with `-explain`
29+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:40:50 -----------------------------------------------------------
30+
40 | def co_too_wide(xs: Co[? <: Any]): Co[Holder] = xs // error
31+
| ^^
32+
| Found: (xs : Test.Co[?])
33+
| Required: Test.Co[Test.Holder]
34+
|
35+
| longer explanation available when compiling with `-explain`
36+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:56:73 -----------------------------------------------------------
37+
56 | def co_guard_upper_boundary(xs: CoBounded[? <: Any]): CoBounded[Sub] = xs // error
38+
| ^^
39+
| Found: (xs : Test.CoBounded[?])
40+
| Required: Test.CoBounded[Test.Sub]
41+
|
42+
| longer explanation available when compiling with `-explain`
43+
-- [E007] Type Mismatch Error: tests/neg/i16018c.scala:68:91 -----------------------------------------------------------
44+
68 | def contra_guard_lower_boundary(xs: ContraBounded[? >: Nothing]): ContraBounded[Super] = xs // error
45+
| ^^
46+
| Found: (xs : Test.ContraBounded[?])
47+
| Required: Test.ContraBounded[Test.Super]
48+
|
49+
| longer explanation available when compiling with `-explain`

tests/neg/i16018c.scala

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Negative half of the directional matrix for the fix to
2+
// `TypeComparer.compareCaptured` (#16018 / Existential widening for wildcard
3+
// arguments).
4+
//
5+
// These shapes must remain *rejected* after the fix: the fix only adds
6+
// widening that is justified by variance — covariant + upper bound,
7+
// contravariant + lower bound. Any other combination is unsound and must
8+
// keep the same diagnostic it had before. If any of these `//<space>error`
9+
// markers stops firing, the fix has overreached.
10+
11+
object Test:
12+
13+
class Co[+T]
14+
class Contra[-T]
15+
class Inv[T]
16+
17+
// ---- A. Invariant containers never widen the wildcard ---------------
18+
def inv_upper_bad[M](xs: Inv[? <: M]): Inv[M] = xs // error
19+
def inv_lower_bad[M](xs: Inv[? >: M]): Inv[M] = xs // error
20+
21+
// ---- B. Covariant with a *lower*-bounded wildcard does not widen ----
22+
//
23+
// For covariant F, `F[? >: lo]` is the type of F[X] for some X >: lo.
24+
// That has no useful relationship to F[lo] — picking X = Any is
25+
// permitted yet gives F[Any], not F[lo].
26+
def co_lower_bad[M](xs: Co[? >: M]): Co[M] = xs // error
27+
28+
// ---- C. Contravariant with an *upper*-bounded wildcard does not widen
29+
//
30+
// Dual to (B): for contravariant G, `G[? <: hi]` makes no commitment
31+
// that the picked X is `hi` itself; it could be `Nothing`.
32+
def contra_upper_bad[M](xs: Contra[? <: M]): Contra[M] = xs // error
33+
34+
// ---- D. The required type's hi is strictly tighter than the wildcard
35+
//
36+
// `Co[? <: Any]` is the trivial widening shape (any T). It must not be
37+
// accepted at `Co[Holder]` because the wildcard's hi (`Any`) is wider
38+
// than the target.
39+
trait Holder
40+
def co_too_wide(xs: Co[? <: Any]): Co[Holder] = xs // error
41+
42+
// ---- E. Upper-boundary check, covariant ------------------------------
43+
//
44+
// Boundary companion to `soundness_guard_co` in tests/pos/i16018c.scala.
45+
// For `CoBounded[+T <: Holder]` fed `CoBounded[? <: Any]`, the effective
46+
// wildcard hi computed by the fix is `Any & Holder = Holder`. The
47+
// *positive* counterpart pins conformance at `CoBounded[Holder]`
48+
// (admits THE FIX, rejects NAIVE). This negative pins that conformance
49+
// stops there: `CoBounded[Sub]` for `Sub <: Holder` must still be
50+
// rejected. Note that all three of OLD / NAIVE / THE FIX reject this
51+
// case (each computes a different LHS but each LHS is ⊄ Sub), so this
52+
// case is a *boundary* test, not a distinguishing one — read the pos
53+
// counterpart for the implementation-distinguishing check.
54+
class Sub extends Holder
55+
class CoBounded[+T <: Holder]
56+
def co_guard_upper_boundary(xs: CoBounded[? <: Any]): CoBounded[Sub] = xs // error
57+
58+
// ---- F. Lower-boundary check, contravariant --------------------------
59+
//
60+
// Dual to (E). The positive counterpart `soundness_guard_contra` pins
61+
// conformance at `ContraBounded[Holder]`. This negative pins that a
62+
// *strict supertype* of `Holder` is not reached (variance flips the
63+
// direction). Same caveat as (E): all three implementations reject,
64+
// so this is a boundary check.
65+
class Super
66+
class HolderS extends Super
67+
class ContraBounded[-T >: HolderS]
68+
def contra_guard_lower_boundary(xs: ContraBounded[? >: Nothing]): ContraBounded[Super] = xs // error

tests/pos/i16018-orig.scala

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// The original akka-minimized reproduction from #16018 (verbatim, modulo
2+
// `_` → `?` syntax migration). Posted by @He-Pin on 2022-09-17:
3+
//
4+
// https://github.com/scala/scala3/issues/16018#issuecomment-1250066489
5+
//
6+
// Pre-fix this rejected with
7+
//
8+
// Found: (other : ?1.CAP)
9+
// Required: Main.Source[T, M] & ?1.CAP
10+
// where: ?1 is an unknown value of type
11+
// scala.runtime.TypeBox[Nothing, Main.Graph[Main.SourceShape[T], ? <: M]]
12+
//
13+
// The directional minimizations of this same shape live in
14+
// tests/pos/i16018b.scala (the matrix); this file pins the exact
15+
// reproduction so that the original ticket cannot regress in either
16+
// direction.
17+
18+
import scala.collection.immutable
19+
20+
object Main:
21+
22+
class Source[+Out, +Mat] extends Graph[SourceShape[Out], Mat]
23+
24+
class Shape
25+
class SourceShape[+T] extends Shape
26+
class Graph[+S <: Shape, +M]
27+
28+
def combine[T, U, M](sources: java.util.List[? <: Graph[SourceShape[T], ? <: M]])
29+
: Source[U, java.util.List[M]] =
30+
val seq: immutable.Seq[Graph[SourceShape[T], M]] =
31+
if sources != null then
32+
immutableSeq(sources).collect {
33+
case source: Source[T, M] @unchecked => source
34+
case other => other
35+
}
36+
else immutable.Seq()
37+
???
38+
39+
def immutableSeq[T](iterable: java.lang.Iterable[T]): immutable.Seq[T] = ???

tests/pos/i16018.scala

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Regression test for #16018: type inference with `_ <: T` wildcards in
2+
// pattern matching over capture-converted scrutinees.
3+
//
4+
// Coverage axes for the positive matrix:
5+
// 1. The mbovel minimization (single wildcard via List.map) — was fixed
6+
// since 3.2.0, kept here as a pin.
7+
// 2. Pure-Scala collections — never trigger the capture-conversion path.
8+
// 3. Identity / generic Scala helpers over the wildcard-bearing collection.
9+
//
10+
// The dual cases reached through Java-generic interop (java.lang.Iterable /
11+
// java.util.List), which were the original akka regression, are in
12+
// tests/pos/i16018b.scala.
13+
14+
object Test:
15+
16+
class Box[T](val value: T)
17+
class Container[+S, +M]
18+
class SubContainer[+S, +M] extends Container[S, M]
19+
20+
// ---- 1. mbovel's minimized case from #16018 ------------------------
21+
def f1[T](l: List[Box[? <: T]]): List[T] = l.map(_.value)
22+
23+
// 1b. Variant: returning the wildcard-bearing element preserved.
24+
def f1b[T](l: List[Box[? <: T]]): List[Box[? <: T]] = l.map(identity)
25+
26+
// ---- 2. Pure-Scala scrutinee — never goes through capture conversion -
27+
def f2[M](xs: scala.collection.immutable.Seq[Container[Any, ? <: M]])
28+
: scala.collection.immutable.Seq[Container[Any, M]] =
29+
xs.collect {
30+
case g: SubContainer[Any, M] @unchecked => g
31+
case other => other
32+
}
33+
34+
// 2b. Variant: `.map` instead of `.collect`.
35+
def f2b[M](xs: scala.collection.immutable.Seq[Container[Any, ? <: M]])
36+
: scala.collection.immutable.Seq[Container[Any, M]] =
37+
xs.map {
38+
case g: SubContainer[Any, M] @unchecked => g
39+
case other => other
40+
}
41+
42+
// 2c. Variant: List instead of Seq.
43+
def f2c[M](xs: List[Container[Any, ? <: M]])
44+
: List[Container[Any, M]] =
45+
xs.collect {
46+
case g: SubContainer[Any, M] @unchecked => g
47+
case other => other
48+
}
49+
50+
// ---- 3. Generic helper that stays pure-Scala does not skolemize ----
51+
def idSeq[T](it: Iterable[T]): scala.collection.immutable.Seq[T] = ???
52+
53+
def f3[M](xs: List[Container[Any, ? <: M]])
54+
: scala.collection.immutable.Seq[Container[Any, M]] =
55+
idSeq(xs).collect {
56+
case g: SubContainer[Any, M] @unchecked => g
57+
case other => other
58+
}

tests/pos/i16018b.scala

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Regression test for #16018: the akka-derived shapes.
2+
//
3+
// These exercise the path that motivated the original ticket: a value of
4+
// type `J[? <: G[? <: M]]` (J a Java generic) flows through a method that
5+
// re-projects it as a Scala collection, and is then `collect`/`map`-ed with
6+
// a subtype pattern + a fallback `case other`. The combination triggers
7+
// (i) `Inferencing.captureWildcards` — the Java-generic argument becomes
8+
// a TypeBox.CAP skolem `?N.CAP`, and
9+
// (ii) `Match` typing — pattern narrowing turns the first case body into
10+
// `pat ∩ ?N.CAP`, while `case other` keeps the raw `?N.CAP`.
11+
//
12+
// Until the fix in `TypeComparer.compareCaptured` for wildcard-bound
13+
// existential widening, the inferred result type could not escape the
14+
// skolem cleanly. See:
15+
// - tests/pos/i16018.scala — companion pos matrix (pure-Scala)
16+
// - scala/scala3 #16018 — original bug report
17+
18+
object Test:
19+
20+
class Container[+S, +M]
21+
class SubContainer[+S, +M] extends Container[S, M]
22+
23+
def seqOf[T](it: java.lang.Iterable[T]): scala.collection.immutable.Seq[T] = ???
24+
25+
// ---- f1: trigger via java.lang.Iterable ----------------------------
26+
def f1[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
27+
: scala.collection.immutable.Seq[Container[Any, M]] =
28+
seqOf(xs).collect {
29+
case g: SubContainer[Any, M] @unchecked => g
30+
case other => other
31+
}
32+
33+
// ---- f2: same trigger via java.util.List ---------------------------
34+
def f2[M](xs: java.util.List[? <: Container[Any, ? <: M]])
35+
: scala.collection.immutable.Seq[Container[Any, M]] =
36+
seqOf(xs).collect {
37+
case g: SubContainer[Any, M] @unchecked => g
38+
case other => other
39+
}
40+
41+
// ---- f3: same trigger via .map instead of .collect -----------------
42+
def f3[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
43+
: scala.collection.immutable.Seq[Container[Any, M]] =
44+
seqOf(xs).map {
45+
case g: SubContainer[Any, M] @unchecked => g
46+
case other => other
47+
}

tests/pos/i16018c.scala

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Directional matrix for the fix to `TypeComparer.compareCaptured`
2+
// (#16018 / Existential widening for wildcard arguments).
3+
//
4+
// The fix replaces `paramBounds(tparam).hi` (the *declared* parameter
5+
// bound) with `arg1.hi & paramBounds(tparam).hi` (the wildcard's own
6+
// upper bound, intersected with the declared one to preserve soundness).
7+
// Dually for contravariance, with `arg1.lo | paramBounds(tparam).lo`.
8+
//
9+
// Three implementations are distinguishable on this matrix:
10+
//
11+
// OLD : isSubType( paramBounds(tparam).hi, arg2 )
12+
// NAIVE : isSubType( arg1.hi, arg2 )
13+
// THE FIX : isSubType( arg1.hi & paramBounds(tparam).hi, arg2 )
14+
//
15+
// Sections 1–3 below distinguish OLD from THE FIX (these are the cases
16+
// the original ticket cares about — OLD rejects, THE FIX accepts).
17+
// Sections 4–5 distinguish a hypothetical NAIVE refactor (drop the
18+
// intersection / union with `paramBounds(tparam)`) from THE FIX — they
19+
// pin the *declared-bound guard*, which is a completeness property
20+
// (NAIVE produces *false negatives* on `CoBounded[? <: Any] <:< CoBounded[Holder]`
21+
// while THE FIX accepts), not a soundness property.
22+
23+
object Test:
24+
25+
class Co[+T]
26+
class Contra[-T]
27+
28+
// ---- 1. Covariant + upper-bounded wildcard widens to its hi --------
29+
//
30+
// For `Co[+T]`, `paramBounds(tparam) = [Nothing, Any]`. OLD asked
31+
// `Any <: M` (false). THE FIX asks `M & Any <: M` (true).
32+
33+
def co_upper_direct[M](xs: Co[? <: M]): Co[M] = xs
34+
def co_upper_in_co[M](xs: Co[Co[? <: M]]): Co[Co[M]] = xs
35+
def co_upper_in_seq[M](xs: scala.collection.immutable.Seq[Co[? <: M]])
36+
: scala.collection.immutable.Seq[Co[M]] = xs
37+
def co_upper_via_tparam[A, M <: A](xs: Co[? <: M]): Co[M] = xs
38+
39+
// ---- 2. Contravariant + lower-bounded wildcard widens to its lo ----
40+
//
41+
// Dual to (1). For `Contra[-T]`, `paramBounds(tparam) = [Nothing, Any]`,
42+
// so `paramBounds(tparam).lo = Nothing`. OLD asked `M <: Nothing`
43+
// (false). THE FIX asks `M <: Nothing | M = M` (true).
44+
45+
def contra_lower_direct[M](xs: Contra[? >: M]): Contra[M] = xs
46+
def contra_lower_in_co[M](xs: Co[Contra[? >: M]]): Co[Contra[M]] = xs
47+
48+
// ---- 3. The headline regression --------------------------------------
49+
//
50+
// The wildcard hi is reached through a covariant container; this is
51+
// the shape that drove the akka report and the pure-Scala minimization
52+
// in tests/pos/i16018.scala.
53+
54+
trait Holder
55+
def headline(xs: Co[? <: Holder]): Co[Holder] = xs
56+
57+
// ---- 4. Declared-bound guard, covariant (intersection in action) -----
58+
//
59+
// For `CoBounded[+T <: Holder]`, `paramBounds(tparam).hi = Holder`. We
60+
// feed a wildcard `? <: Any` whose hi (`Any`) is *wider* than the
61+
// declared bound. The three candidates diverge:
62+
//
63+
// OLD : isSubType( Holder, Holder ) = true (would pass)
64+
// NAIVE : isSubType( Any, Holder ) = false (would fail)
65+
// THE FIX : isSubType( Any & Holder = Holder, Holder ) = true
66+
//
67+
// The case must pass: a value of `CoBounded[? <: Any]` is, by the
68+
// parameter's own constraint, a value of `CoBounded[X]` for some
69+
// `X <: Holder`, hence (by covariance) of `CoBounded[Holder]`.
70+
// The intersection `& paramBounds(tparam).hi` is what saves THE FIX
71+
// from inheriting NAIVE's *false negative* here. (Note: vs OLD this is
72+
// a completeness improvement, since OLD also accepts this case; vs
73+
// NAIVE this distinguishes THE FIX as the only one that does.)
74+
75+
class CoBounded[+T <: Holder]
76+
def declared_bound_guard_co(xs: CoBounded[? <: Any]): CoBounded[Holder] = xs
77+
78+
// ---- 5. Declared-bound guard, contravariant (union in action) --------
79+
//
80+
// Dual to (4). For `ContraBounded[-T >: Holder]`,
81+
// `paramBounds(tparam).lo = Holder`. We feed a wildcard `? >: Nothing`
82+
// whose lo (`Nothing`) is *lower* than the declared bound. The three
83+
// candidates diverge:
84+
//
85+
// OLD : isSubType( Holder, Holder ) = true (would pass)
86+
// NAIVE : isSubType( Holder, Nothing ) = false (would fail)
87+
// THE FIX : isSubType( Holder, Nothing | Holder = Holder ) = true
88+
//
89+
// The union `| paramBounds(tparam).lo` is what saves THE FIX from
90+
// NAIVE's *false negative*. Same completeness caveat as (4).
91+
92+
class ContraBounded[-T >: Holder]
93+
def declared_bound_guard_contra(xs: ContraBounded[? >: Nothing]): ContraBounded[Holder] = xs

0 commit comments

Comments
 (0)