Skip to content

Commit 849ee9c

Browse files
authored
In 3.4 make refutable patterns in a for comprehension an error (#18842)
supersedes #16665 Only make refutable patterns in a for comprehension an error, here we have a clear set in stone solution: put `case` before the pattern. It is still in the air the ideal solution for pattern val definitions, see https://contributors.scala-lang.org/t/pre-sip-replace-non-sensical-unchecked-annotations/6342/85, so keep those as a warning for now
2 parents cee2610 + 0bfd343 commit 849ee9c

19 files changed

+137
-57
lines changed

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -2772,7 +2772,7 @@ object Parsers {
27722772
atSpan(startOffset(pat), accept(LARROW)) {
27732773
val checkMode =
27742774
if casePat then GenCheckMode.FilterAlways
2775-
else if sourceVersion.isAtLeast(`future`) then GenCheckMode.Check
2775+
else if sourceVersion.isAtLeast(`3.4`) then GenCheckMode.Check
27762776
else if sourceVersion.isAtLeast(`3.2`) then GenCheckMode.CheckAndFilter
27772777
else GenCheckMode.FilterNow // filter on source version < 3.2, for backward compat
27782778
GenFrom(pat, subExpr(), checkMode)

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,13 @@ trait Checking {
923923
|
924924
|If $usage is intentional, this can be communicated by $fix,
925925
|which $addendum.$rewriteMsg"""),
926-
pos, warnFrom = `3.2`, errorFrom = `future`)
926+
pos,
927+
warnFrom = `3.2`,
928+
// we tighten for-comprehension without `case` to error in 3.4,
929+
// but we keep pat-defs as warnings for now ("@unchecked"),
930+
// until we propose an alternative way to assert exhaustivity to the typechecker.
931+
errorFrom = if isPatDef then `future` else `3.4`
932+
)
927933
false
928934
}
929935

project/Build.scala

+1
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,7 @@ object Build {
14931493
(
14941494
(dir / "shared/src/test/scala" ** (("*.scala": FileFilter)
14951495
-- "ReflectiveCallTest.scala" // uses many forms of structural calls that are not allowed in Scala 3 anymore
1496+
-- "UTF16Test.scala" // refutable pattern match
14961497
)).get
14971498

14981499
++ (dir / "shared/src/test/require-sam" ** "*.scala").get

tests/neg/irrefutable.check

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- [E008] Not Found Error: tests/neg/irrefutable.scala:27:29 -----------------------------------------------------------
2+
27 | for (case Foo(x: Int) <- xs) yield x // error
3+
| ^^
4+
| value withFilter is not a member of Lst[Foo[Any]]
5+
-- Error: tests/neg/irrefutable.scala:30:16 ----------------------------------------------------------------------------
6+
30 | for (Foo(x: Int) <- xs) yield x // error
7+
| ^^^
8+
| pattern's type Int is more specialized than the right hand side expression's type Any
9+
|
10+
| If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
11+
| which will result in a filtering for expression (using `withFilter`).
12+
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.

tests/neg/irrefutable.scala

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// This tests that A.f1 is recognized as an irrefutable pattern and A.f2_nocase is not, and therefore A.f2 solves this
2+
// by adding a case to the pattern, which results in withFilter being inserted.
3+
// see also: tests/run/irrefutable.scala for an example that exercises the insertion of withFilter.
4+
5+
class Lst[+T](val id: String, val underlying: List[T]) {
6+
def map[U](f: T => U): Lst[U] = new Lst(id, underlying.map(f))
7+
8+
// hide the withFilter so that there is a compile error
9+
// def withFilter(f: T => Boolean): Lst.WithFilter[T] = new Lst.WithFilter(this, f)
10+
}
11+
12+
// object Lst:
13+
// class WithFilter[+T](lst: Lst[T], filter: T => Boolean):
14+
// def forwardingFilter[T1](filter: T1 => Boolean): T1 => Boolean = t =>
15+
// println(s"filtering $t in ${lst.id}")
16+
// filter(t)
17+
18+
// def map[U](f: T => U): Lst[U] = Lst(lst.id, lst.underlying.withFilter(forwardingFilter(filter)).map(f))
19+
20+
case class Foo[T](x: T)
21+
22+
object A {
23+
def f1(xs: Lst[Foo[Int]]): Lst[Int] = {
24+
for (Foo(x: Int) <- xs) yield x
25+
}
26+
def f2(xs: Lst[Foo[Any]]): Lst[Int] = {
27+
for (case Foo(x: Int) <- xs) yield x // error
28+
}
29+
def f2_nocase(xs: Lst[Foo[Any]]): Lst[Int] = {
30+
for (Foo(x: Int) <- xs) yield x // error
31+
}
32+
}
33+
34+
@main def Test =
35+
val xs = new Lst("xs", List(Foo(1), Foo(2), Foo(3)))
36+
println("=== mapping xs with A.f1 ===")
37+
val xs1 = A.f1(xs)
38+
assert(xs1.underlying == List(1, 2, 3))
39+
val ys = new Lst("ys", List(Foo(1: Any), Foo(2: Any), Foo(3: Any)))
40+
println("=== mapping ys with A.f2 ===")
41+
val ys1 = A.f2(ys)
42+
assert(ys1.underlying == List(1, 2, 3))

tests/neg/refutable-pattern-binding-messages.check

+16-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
-- Error: tests/neg/refutable-pattern-binding-messages.scala:5:14 ------------------------------------------------------
2-
5 | val Positive(p) = 5 // error: refutable extractor
3-
| ^^^^^^^^^^^^^^^
4-
| pattern binding uses refutable extractor `Test.Positive`
5-
|
6-
| If this usage is intentional, this can be communicated by adding `: @unchecked` after the expression,
7-
| which may result in a MatchError at runtime.
8-
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
91
-- Error: tests/neg/refutable-pattern-binding-messages.scala:6:14 ------------------------------------------------------
102
6 | for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor
113
| ^^^^^^^^^^^
@@ -14,14 +6,6 @@
146
| If this usage is intentional, this can be communicated by adding the `case` keyword before the full pattern,
157
| which will result in a filtering for expression (using `withFilter`).
168
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
17-
-- Error: tests/neg/refutable-pattern-binding-messages.scala:10:20 -----------------------------------------------------
18-
10 | val i :: is = List(1, 2, 3) // error: pattern type more specialized
19-
| ^^^^^^^^^^^^^
20-
| pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int]
21-
|
22-
| If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
23-
| which may result in a MatchError at runtime.
24-
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
259
-- Error: tests/neg/refutable-pattern-binding-messages.scala:11:11 -----------------------------------------------------
2610
11 | for ((x: String) <- xs) do () // error: pattern type more specialized
2711
| ^^^^^^
@@ -38,6 +22,22 @@
3822
| If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern,
3923
| which will result in a filtering for expression (using `withFilter`).
4024
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
25+
-- Error: tests/neg/refutable-pattern-binding-messages.scala:5:14 ------------------------------------------------------
26+
5 | val Positive(p) = 5 // error: refutable extractor
27+
| ^^^^^^^^^^^^^^^
28+
| pattern binding uses refutable extractor `Test.Positive`
29+
|
30+
| If this usage is intentional, this can be communicated by adding `: @unchecked` after the expression,
31+
| which may result in a MatchError at runtime.
32+
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
33+
-- Error: tests/neg/refutable-pattern-binding-messages.scala:10:20 -----------------------------------------------------
34+
10 | val i :: is = List(1, 2, 3) // error: pattern type more specialized
35+
| ^^^^^^^^^^^^^
36+
| pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int]
37+
|
38+
| If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
39+
| which may result in a MatchError at runtime.
40+
| This patch can be rewritten automatically under -rewrite -source 3.2-migration.
4141
-- Error: tests/neg/refutable-pattern-binding-messages.scala:16:10 -----------------------------------------------------
4242
16 | val 1 = 2 // error: pattern type does not match
4343
| ^

tests/pos/irrefutable.scala

-22
This file was deleted.

tests/pos/t6205.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
class A[T]
33
class Test1 {
44
def x(backing: Map[A[_], Any]) =
5-
for( (k: A[kt], v) <- backing)
5+
for(case (k: A[kt], v) <- backing)
66
yield (k: A[kt])
77
}
88

tests/run/ReplacementMatching.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ object Test {
3232

3333
def groupsMatching: Unit = {
3434
val Date = """(\d+)/(\d+)/(\d+)""".r
35-
for (Regex.Groups(a, b, c) <- Date findFirstMatchIn "1/1/2001 marks the start of the millennium. 31/12/2000 doesn't.") {
35+
for (case Regex.Groups(a, b, c) <- Date findFirstMatchIn "1/1/2001 marks the start of the millennium. 31/12/2000 doesn't.") {
3636
assert(a == "1")
3737
assert(b == "1")
3838
assert(c == "2001")
3939
}
40-
for (Regex.Groups(a, b, c) <- (Date findAllIn "1/1/2001 marks the start of the millennium. 31/12/2000 doesn't.").matchData) {
40+
for (case Regex.Groups(a, b, c) <- (Date findAllIn "1/1/2001 marks the start of the millennium. 31/12/2000 doesn't.").matchData) {
4141
assert(a == "1" || a == "31")
4242
assert(b == "1" || b == "12")
4343
assert(c == "2001" || c == "2000")

tests/run/irrefutable.check

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
=== mapping xs with A.f1 ===
2+
=== mapping ys with A.f2 ===
3+
filtering Foo(1) in ys
4+
filtering Foo(2) in ys
5+
filtering Foo(3) in ys

tests/run/irrefutable.scala

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// This tests that A.f1 does not filter its inputs, whereas A.f2 does.
2+
// see also: tests/neg/irrefutable.scala for an example that exercises the requirement to insert case.
3+
4+
class Lst[+T](val id: String, val underlying: List[T]) {
5+
def map[U](f: T => U): Lst[U] = new Lst(id, underlying.map(f))
6+
def withFilter(f: T => Boolean): Lst.WithFilter[T] = new Lst.WithFilter(this, f)
7+
}
8+
9+
object Lst:
10+
class WithFilter[+T](lst: Lst[T], filter: T => Boolean):
11+
def forwardingFilter[T1](filter: T1 => Boolean): T1 => Boolean = t =>
12+
println(s"filtering $t in ${lst.id}")
13+
filter(t)
14+
15+
def map[U](f: T => U): Lst[U] = Lst(lst.id, lst.underlying.withFilter(forwardingFilter(filter)).map(f))
16+
17+
case class Foo[T](x: T)
18+
19+
object A {
20+
def f1(xs: Lst[Foo[Int]]): Lst[Int] = {
21+
for (Foo(x: Int) <- xs) yield x
22+
}
23+
def f2(xs: Lst[Foo[Any]]): Lst[Int] = {
24+
for (case Foo(x: Int) <- xs) yield x
25+
}
26+
}
27+
28+
@main def Test =
29+
val xs = new Lst("xs", List(Foo(1), Foo(2), Foo(3)))
30+
println("=== mapping xs with A.f1 ===")
31+
val xs1 = A.f1(xs)
32+
assert(xs1.underlying == List(1, 2, 3))
33+
val ys = new Lst("ys", List(Foo(1: Any), Foo(2: Any), Foo(3: Any)))
34+
println("=== mapping ys with A.f2 ===")
35+
val ys1 = A.f2(ys)
36+
assert(ys1.underlying == List(1, 2, 3))

tests/run/patmat-bind-typed.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
object Test {
2-
def f(xs: List[Any]) = for (key @ (dummy: String) <- xs) yield key
2+
def f(xs: List[Any]) = for (case key @ (dummy: String) <- xs) yield key
33

44
def main(args: Array[String]): Unit = {
55
f("abc" :: Nil) foreach println

tests/run/quoted-sematics-1.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,24 @@ def typeChecks(g: Gamma)(level: 0 | 1)(term: Term): Option[Type] =
8282
yield LambdaType(t, res)
8383
case App(fun, arg) => // T-App
8484
for
85-
LambdaType(t1, t2) <- typeChecks(g)(level)(fun)
85+
case LambdaType(t1, t2) <- typeChecks(g)(level)(fun)
8686
`t1` <- typeChecks(g)(level)(arg)
8787
yield t2
8888
case Box(body) if level == 0 => // T-Box
8989
for t <- typeChecks(g)(1)(body) yield BoxType(t)
9090
case Lift(body) if level == 0 => // T-Lift
9191
for NatType <- typeChecks(g)(0)(body) yield BoxType(NatType)
9292
case Splice(body) if level == 1 => // T-Unbox
93-
for BoxType(t) <- typeChecks(g)(0)(body) yield t
93+
for case BoxType(t) <- typeChecks(g)(0)(body) yield t
9494
case Match(scrutinee, pat, thenp, elsep) => // T-Pat
9595
for
96-
BoxType(t1) <- typeChecks(g)(0)(scrutinee)
96+
case BoxType(t1) <- typeChecks(g)(0)(scrutinee)
9797
delta <- typePatChecks(g, t1)(pat)
9898
t <- typeChecks(g ++ delta)(0)(thenp)
9999
`t` <- typeChecks(g)(0)(elsep)
100100
yield t
101101
case Fix(t) if level == 0 =>
102-
for LambdaType(t1, t2) <- typeChecks(g)(0)(t) yield t2 // T-Fix
102+
for case LambdaType(t1, t2) <- typeChecks(g)(0)(t) yield t2 // T-Fix
103103
case _ => None
104104
if res.isEmpty then
105105
println(s"Failed to type $term at level $level with environment $g")

tests/run/t6406-regextract.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ object Test extends App {
2020
val t = "Last modified 2011-07-15"
2121
val p1 = """(\d\d\d\d)-(\d\d)-(\d\d)""".r
2222
val y1: Option[String] = for {
23-
p1(year, month, day) <- p1 findFirstIn t
23+
case p1(year, month, day) <- p1 findFirstIn t
2424
} yield year
2525
val y2: Option[String] = for {
26-
p1(year, month, day) <- p1 findFirstMatchIn t
26+
case p1(year, month, day) <- p1 findFirstMatchIn t
2727
} yield year
2828
println(s"$y1 $y2")
2929

tests/run/t6646.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ object Test {
88
val l = List(PrimaryKey, NoNull, lower)
99

1010
// withFilter must be generated in these
11-
for (option @ NoNull <- l) println("Found " + option)
12-
for (option @ `lower` <- l) println("Found " + option)
13-
for ((`lower`, i) <- l.zipWithIndex) println("Found " + i)
11+
for (case option @ NoNull <- l) println("Found " + option)
12+
for (case option @ `lower` <- l) println("Found " + option)
13+
for (case (`lower`, i) <- l.zipWithIndex) println("Found " + i)
1414

1515
// no withFilter
1616
for (X <- List("A single ident is always a pattern")) println(X)

tests/run/t6968.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
object Test {
22
def main(args: Array[String]): Unit = {
33
val mixedList = List(1,(1,2),4,(3,1),(5,4),6)
4-
val as = for((a,b) <- mixedList) yield a
4+
val as = for(case (a,b) <- mixedList) yield a
55
println(as.mkString(", "))
66
}
77
}

0 commit comments

Comments
 (0)