Skip to content

Commit b45a8f5

Browse files
committed
Pin partial progress of #16018 with directional pos/neg tests
#16018 is a long-standing typer regression from Scala 2: passing a value of type `J[? <: G[? <: M]]` (where J is a Java generic such as java.lang.Iterable / java.util.List) through a method, then pattern- matching the result with a subtype pattern plus a fallback `case other`, breaks partial-function return-type inference. The mbovel-style minimized case (a wildcard reached through `List.map`) already compiles since 3.2.0. The akka-style case (wildcard reached through Java-generic interop, with subtype + fallback patterns) still fails on current main. The two halves form a directional matrix that flips green/red as soon as either side moves -- ideal for catching both forward fixes and silent regressions. Files: - tests/pos/i16018.scala f1: mbovel's minimized case -- regression test for the fixed half. f2: pure-Scala same-shape control -- confirms the bug is not in the wildcard-bearing parameter alone. f3: documents the explicit-widening workaround (`case g: SubT[...] @unchecked => (g: WiderT[...])`) so user code that adopts it does not silently break later. - tests/neg/i16018.scala (+ .check) f1: java.lang.Iterable trigger. f2: java.util.List trigger. f3: same trigger via `.map` instead of `.collect`, showing it is not specific to partial functions over `collect`. The error pattern is always identical: B := SubType[...] & {?N.CAP | xs.T} i.e. the partial-function result type is constrained to the first case's narrowed body type instead of the LUB of all case bodies. When the underlying typer bug is fixed, the neg cases should compile and be moved to tests/pos/i16018.scala. No compiler changes; tests only.
1 parent b6e5747 commit b45a8f5

3 files changed

Lines changed: 162 additions & 0 deletions

File tree

tests/neg/i16018.check

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i16018.scala:29:49 ------------------------------------------------------------
2+
29 | case other => other // error
3+
| ^^^^^
4+
| Found: (other : xs.T)
5+
| Required: Test.SubContainer[Any, M] & xs.T
6+
|
7+
| longer explanation available when compiling with `-explain`
8+
-- [E007] Type Mismatch Error: tests/neg/i16018.scala:39:49 ------------------------------------------------------------
9+
39 | case other => other // error
10+
| ^^^^^
11+
| Found: (other : ?1.CAP)
12+
| Required: Test.SubContainer[Any, M] & ?1.CAP
13+
|
14+
| where: ?1 is an unknown value of type scala.runtime.TypeBox[Nothing, Test.Container[Any, ? <: M]]
15+
|
16+
| longer explanation available when compiling with `-explain`
17+
-- [E007] Type Mismatch Error: tests/neg/i16018.scala:47:49 ------------------------------------------------------------
18+
47 | case other => other // error
19+
| ^^^^^
20+
| Found: (other : xs.T)
21+
| Required: Test.SubContainer[Any, M] & xs.T
22+
|
23+
| longer explanation available when compiling with `-explain`

tests/neg/i16018.scala

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Documents the unfixed half of #16018.
2+
//
3+
// Each `f*` is a minimal *failing* shape that the bug report from akka
4+
// boils down to. Together with tests/pos/i16018.scala they pin the
5+
// trigger: passing a value of type `J[? <: G[? <: M]]` (where J is a
6+
// Java generic such as java.lang.Iterable / java.util.List) through a
7+
// method, then pattern-matching the result with a subtype pattern + a
8+
// fallback `case other`, breaks partial-function return-type inference.
9+
//
10+
// The error pattern is always the same: B := `SubType[...] & ?N.CAP`
11+
// instead of the LUB of the case bodies, where `?N.CAP` is a
12+
// capture-conversion skolem with bounds `[Nothing, G[? <: M]]`.
13+
//
14+
// When the underlying typer bug is fixed, these cases will start to
15+
// compile and should be moved to tests/pos/i16018.scala.
16+
17+
object Test:
18+
19+
class Container[+S, +M]
20+
class SubContainer[+S, +M] extends Container[S, M]
21+
22+
def seqOf[T](it: java.lang.Iterable[T]): scala.collection.immutable.Seq[T] = ???
23+
24+
// ---- f1: minimum trigger via java.lang.Iterable --------------------
25+
def f1[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
26+
: scala.collection.immutable.Seq[Container[Any, M]] =
27+
seqOf(xs).collect {
28+
case g: SubContainer[Any, M] @unchecked => g
29+
case other => other // error
30+
}
31+
32+
// ---- f2: same trigger via java.util.List ---------------------------
33+
// Confirms it is not specific to Iterable but to any Java generic
34+
// carrying the wildcard-bearing element type.
35+
def f2[M](xs: java.util.List[? <: Container[Any, ? <: M]])
36+
: scala.collection.immutable.Seq[Container[Any, M]] =
37+
seqOf(xs).collect {
38+
case g: SubContainer[Any, M] @unchecked => g
39+
case other => other // error
40+
}
41+
42+
// ---- f3: same trigger via .map (not specific to collect) -----------
43+
def f3[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
44+
: scala.collection.immutable.Seq[Container[Any, M]] =
45+
seqOf(xs).map {
46+
case g: SubContainer[Any, M] @unchecked => g
47+
case other => other // error
48+
}

tests/pos/i16018.scala

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Regression test for #16018: type inference with `_ <: T` wildcards.
2+
//
3+
// Each `f*` exercises a *known-good* shape that the compiler must keep
4+
// inferring correctly. The complementary `f*` shapes that currently
5+
// regress are pinned in tests/neg/i16018.scala. Together they form a
6+
// directional matrix that flips green/red as soon as either side moves.
7+
//
8+
// Coverage axes for the positive matrix:
9+
// 1. Bug origin: mbovel's minimized case (was broken in 3.1.x).
10+
// 2. Pure-Scala collections never trigger the bug, regardless of
11+
// wildcard depth or subtype-pattern interaction.
12+
// 3. Identity / generic helper methods over Scala types do not
13+
// introduce skolems and therefore stay safe.
14+
// 4. The widening workaround for the akka-style failure must keep
15+
// compiling so user code that adopts it does not silently break.
16+
17+
object Test:
18+
19+
class Box[T](val value: T)
20+
class Container[+S, +M]
21+
class SubContainer[+S, +M] extends Container[S, M]
22+
23+
// ---- 1. mbovel's minimized case from #16018 ------------------------
24+
// Was broken in 3.1.1 and 3.1.3, fixed since 3.2.0.
25+
def f1[T](l: List[Box[? <: T]]): List[T] = l.map(_.value)
26+
27+
// 1b. Variant: returning the wildcard-bearing element preserved.
28+
def f1b[T](l: List[Box[? <: T]]): List[Box[? <: T]] = l.map(identity)
29+
30+
// ---- 2. Pure-Scala, no Java involvement ----------------------------
31+
// Identical structure to the failing case but without java.lang.Iterable.
32+
def f2[M](xs: scala.collection.immutable.Seq[Container[Any, ? <: M]])
33+
: scala.collection.immutable.Seq[Container[Any, M]] =
34+
xs.collect {
35+
case g: SubContainer[Any, M] @unchecked => g
36+
case other => other
37+
}
38+
39+
// 2b. Variant: `.map` instead of `.collect` over the same pure-Scala input.
40+
def f2b[M](xs: scala.collection.immutable.Seq[Container[Any, ? <: M]])
41+
: scala.collection.immutable.Seq[Container[Any, M]] =
42+
xs.map {
43+
case g: SubContainer[Any, M] @unchecked => g
44+
case other => other
45+
}
46+
47+
// 2c. Variant: List instead of Seq.
48+
def f2c[M](xs: List[Container[Any, ? <: M]])
49+
: List[Container[Any, M]] =
50+
xs.collect {
51+
case g: SubContainer[Any, M] @unchecked => g
52+
case other => other
53+
}
54+
55+
// ---- 3. Generic helper that stays pure-Scala does not skolemize ----
56+
def idSeq[T](it: Iterable[T]): scala.collection.immutable.Seq[T] = ???
57+
58+
def f3[M](xs: List[Container[Any, ? <: M]])
59+
: scala.collection.immutable.Seq[Container[Any, M]] =
60+
idSeq(xs).collect {
61+
case g: SubContainer[Any, M] @unchecked => g
62+
case other => other
63+
}
64+
65+
// ---- 4. Documented workaround for the akka-style failure -----------
66+
// Until tests/neg/i16018.scala flips to pos, user code must widen the
67+
// narrow-pattern branch explicitly. Guarding this shape ensures the
68+
// workaround itself does not regress.
69+
def seqOf[T](it: java.lang.Iterable[T]): scala.collection.immutable.Seq[T] = ???
70+
71+
def f4_workaround_iterable[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
72+
: scala.collection.immutable.Seq[Container[Any, M]] =
73+
seqOf(xs).collect {
74+
case g: SubContainer[Any, M] @unchecked => (g: Container[Any, M])
75+
case other => other
76+
}
77+
78+
def f4_workaround_list[M](xs: java.util.List[? <: Container[Any, ? <: M]])
79+
: scala.collection.immutable.Seq[Container[Any, M]] =
80+
seqOf(xs).collect {
81+
case g: SubContainer[Any, M] @unchecked => (g: Container[Any, M])
82+
case other => other
83+
}
84+
85+
// 4b. Workaround via .asInstanceOf is equally valid.
86+
def f4_asInstanceOf[M](xs: java.lang.Iterable[? <: Container[Any, ? <: M]])
87+
: scala.collection.immutable.Seq[Container[Any, M]] =
88+
seqOf(xs).collect {
89+
case g: SubContainer[Any, M] @unchecked => g.asInstanceOf[Container[Any, M]]
90+
case other => other
91+
}

0 commit comments

Comments
 (0)