Skip to content
This repository was archived by the owner on Sep 1, 2020. It is now read-only.

Commit 3f7b8b5

Browse files
author
Adriaan Moors
committed
Exhaustivity: TreeMakers as boolean propositions
We check exhaustivity by representing a match as a formula in finite-domain propositional logic (FDPL) that is false when the match may fail. The variables in the formula represent tested trees in the match (type tests/value equality tests). The approximation uses the same framework as the CSE analysis. A matrix of tree makers is turned into a DAG, where sharing represents the same value/type being tested. We reduce FDPL to Boolean PL as follows. For all assignments, V_i = c_i_j, we introduce a proposition P_i_j that is true iff V_i is equal to the constant c_i_j, for a given i, and all j, P_i_j are mutually exclusive (a variable cannot have multiple values). If the variable's domain is closed, we assert that one of P_i_j must be true for each i and some j. The conjunction of these propositions constitutes the equality axioms. After going through negational normal form to conjunctive normal form, we use a small SAT solver (the DPLL algorithm) to find a model under which the equational axioms hold but the match fails. The formula: EqAxioms /\ -MatchSucceeds. Note that match failure expresses nicely in CNF: the negation of each case (which yields a disjunction) is anded. We then turn this model into variable assignments (what's the variable (not) equal to, along with recursive assignments for its fields). Valid assignments (no ill-typed field assignments) are then presented to the user as counter examples. A counter example is a value, a type test, a constructor call or a wildcard. We prune the example set and only report the most general examples. (Finally, we sort the output to yield stable, i.e. testable, warning messages.) A match is only checked for exhaustivity when the type of the selector is "checkable" (has a sealed type or is a tuple with at least one component of sealed type). We consider statically known guard outcomes, but generally back off (don't check exhaustivity) when a match has guards or user-defined extractor calls. (Sometimes constant folding lets us statically decide a guard.) We ignore possibly failing null checks (which are performed before calling extractors, for example), though this could be done easily in the current framework. The problem is false positives. People don't usually put nulls in tuples or lists. To improve the exhaustivity checks, we rewrite `List()` to Nil. TODO: more general rewrite of List(a, b, ..., z) to `a :: b :: ... :: z`. When presenting counter examples, we represent lists in the user-friendly List(a,...,z) format. (Similarly for tuples.) There are no exhaustivity checks for a match-defined PartialFunction. misc notes: - fix pure case of dpll solver impure set (symbol that occurs both as a positive and negative literal) was always empty since I was looking for literals (which are not equal if positivity is not equal) but should have been looking for symbols - FDPL -> BoolPL translation collects all syms in props since propForEqualsTo generates an Or, must traverse the prop rather than assuming only top-level Syms are relevant... also, propForEqualsTo will not assume Or'ing a whole domain is equivalent to True (which it isn't, since the type test may fail in general) - improve counter example description - treat as constructor call when we either have definite type information about a real class, or we have no equality information at all, but the variable's type is a class and we gathered constraints about its fields (typically when selector is a tuple) - flatten a :: b :: ... :: Nil to List(a, b, ...) - don't print tuple constructor names, so instead of "Tuple2(a, b)", say "(a, b)" - filter out more statically impossible subtypes the static types convey more information than is actually checkable at run time this is good, as it allows us to narrow down the subtypes of a sealed type, however, when modeling the corresponding run-time checks we need to "erase" the uncheckable parts (using existentials so that the types are still well-kinded), so that we can use static subtyping as a sound model for dynamic type checks - experimental java enum handling seals enum class before, we created a refinement class as the placeholder for the sealed children it seems more direct to use the enum class for that this way, the new pattern matcher's exhaustiveness checker just works for java enums
1 parent 0497c15 commit 3f7b8b5

31 files changed

+882
-123
lines changed

src/compiler/scala/reflect/internal/Constants.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ trait Constants extends api.Constants {
224224
case ClazzTag => "classOf[" + signature(typeValue) + "]"
225225
case CharTag => "'" + escapedChar(charValue) + "'"
226226
case LongTag => longValue.toString() + "L"
227+
case EnumTag => symbolValue.name.toString()
227228
case _ => String.valueOf(value)
228229
}
229230
}

src/compiler/scala/reflect/internal/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ trait Definitions extends reflect.api.StandardDefinitions {
609609
def isTupleTypeOrSubtype(tp: Type) = isTupleType(tp)
610610

611611
def tupleField(n: Int, j: Int) = getMember(TupleClass(n), nme.productAccessorName(j))
612+
// NOTE: returns true for NoSymbol since it's included in the TupleClass array -- is this intensional?
612613
def isTupleSymbol(sym: Symbol) = TupleClass contains unspecializedSymbol(sym)
613614
def isProductNClass(sym: Symbol) = ProductClass contains sym
614615

src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -615,12 +615,11 @@ abstract class ClassfileParser {
615615

616616
// sealed java enums (experimental)
617617
if (isEnum && opt.experimental) {
618-
// need to give singleton type
619-
sym setInfo info.narrow
620-
if (!sym.superClass.isSealed)
621-
sym.superClass setFlag SEALED
618+
val enumClass = sym.owner.linkedClassOfClass
619+
if (!enumClass.isSealed)
620+
enumClass setFlag (SEALED | ABSTRACT)
622621

623-
sym.superClass addChild sym
622+
enumClass addChild sym
624623
}
625624
}
626625
}

src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala

Lines changed: 703 additions & 1 deletion
Large diffs are not rendered by default.

src/compiler/scala/tools/nsc/typechecker/Typers.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2327,7 +2327,7 @@ trait Typers extends Modes with Adaptations with Taggings {
23272327
val methodBodyTyper = newTyper(context.makeNewScope(context.tree, methodSym)) // should use the DefDef for the context's tree, but it doesn't exist yet (we need the typer we're creating to create it)
23282328
paramSyms foreach (methodBodyTyper.context.scope enter _)
23292329

2330-
val match_ = methodBodyTyper.typedMatch(selector, cases, mode, ptRes)
2330+
val match_ = methodBodyTyper.typedMatch(gen.mkUnchecked(selector), cases, mode, ptRes)
23312331
val resTp = match_.tpe
23322332

23332333
val methFormals = paramSyms map (_.tpe)
@@ -2367,7 +2367,7 @@ trait Typers extends Modes with Adaptations with Taggings {
23672367
val methodBodyTyper = newTyper(context.makeNewScope(context.tree, methodSym)) // should use the DefDef for the context's tree, but it doesn't exist yet (we need the typer we're creating to create it)
23682368
paramSyms foreach (methodBodyTyper.context.scope enter _)
23692369

2370-
val match_ = methodBodyTyper.typedMatch(selector, cases, mode, ptRes)
2370+
val match_ = methodBodyTyper.typedMatch(gen.mkUnchecked(selector), cases, mode, ptRes)
23712371
val resTp = match_.tpe
23722372

23732373
anonClass setInfo ClassInfoType(parentsPartial(List(argTp, resTp)), newScope, anonClass)
@@ -2394,7 +2394,7 @@ trait Typers extends Modes with Adaptations with Taggings {
23942394
paramSyms foreach (methodBodyTyper.context.scope enter _)
23952395
methodSym setInfoAndEnter MethodType(paramSyms, BooleanClass.tpe)
23962396

2397-
val match_ = methodBodyTyper.typedMatch(selector, casesTrue, mode, BooleanClass.tpe)
2397+
val match_ = methodBodyTyper.typedMatch(gen.mkUnchecked(selector), casesTrue, mode, BooleanClass.tpe)
23982398
val body = methodBodyTyper.virtualizedMatch(match_ withAttachment DefaultOverrideMatchAttachment(FALSE_typed), mode, BooleanClass.tpe)
23992399

24002400
DefDef(methodSym, body)

test/files/neg/exhausting.check

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
1-
exhausting.scala:20: error: match is not exhaustive!
2-
missing combination * Nil
3-
1+
exhausting.scala:21: error: match may not be exhaustive.
2+
It would fail on the following input: List(_, _, _)
43
def fail1[T](xs: List[T]) = xs match {
54
^
6-
exhausting.scala:24: error: match is not exhaustive!
7-
missing combination Nil
8-
5+
exhausting.scala:27: error: match may not be exhaustive.
6+
It would fail on the following input: Nil
97
def fail2[T](xs: List[T]) = xs match {
108
^
11-
exhausting.scala:27: error: match is not exhaustive!
12-
missing combination Bar3
13-
9+
exhausting.scala:32: error: match may not be exhaustive.
10+
It would fail on the following input: List(<not in (1, 2)>)
11+
def fail3a(xs: List[Int]) = xs match {
12+
^
13+
exhausting.scala:39: error: match may not be exhaustive.
14+
It would fail on the following input: Bar3
1415
def fail3[T](x: Foo[T]) = x match {
1516
^
16-
exhausting.scala:31: error: match is not exhaustive!
17-
missing combination Bar2 Bar2
18-
17+
exhausting.scala:47: error: match may not be exhaustive.
18+
It would fail on the following inputs: (Bar1, Bar2), (Bar1, Bar3), (Bar2, Bar1), (Bar2, Bar2)
1919
def fail4[T <: AnyRef](xx: (Foo[T], Foo[T])) = xx match {
2020
^
21-
exhausting.scala:36: error: match is not exhaustive!
22-
missing combination Bar1 Bar2
23-
missing combination Bar1 Bar3
24-
missing combination Bar2 Bar1
25-
missing combination Bar2 Bar2
26-
21+
exhausting.scala:56: error: match may not be exhaustive.
22+
It would fail on the following inputs: (Bar1, Bar2), (Bar1, Bar3), (Bar2, Bar1), (Bar2, Bar2)
2723
def fail5[T](xx: (Foo[T], Foo[T])) = xx match {
2824
^
29-
5 errors found
25+
6 errors found

test/files/neg/exhausting.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xfatal-warnings -Xoldpatmat
1+
-Xfatal-warnings

test/files/neg/exhausting.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,46 @@ object Test {
1616
def ex3[T](xx: (Foo[T], Foo[T])) = xx match {
1717
case (_: Foo[_], _: Foo[_]) => ()
1818
}
19-
19+
20+
// fails for: ::(_, ::(_, ::(_, _)))
2021
def fail1[T](xs: List[T]) = xs match {
2122
case Nil => "ok"
2223
case x :: y :: Nil => "ok"
2324
}
25+
26+
// fails for: Nil
2427
def fail2[T](xs: List[T]) = xs match {
2528
case _ :: _ => "ok"
2629
}
30+
31+
// fails for: ::(<not in (2, 1)>, _)
32+
def fail3a(xs: List[Int]) = xs match {
33+
case 1 :: _ =>
34+
case 2 :: _ =>
35+
case Nil =>
36+
}
37+
38+
// fails for: Bar3
2739
def fail3[T](x: Foo[T]) = x match {
2840
case Bar1 => "ok"
2941
case Bar2 => "ok"
3042
}
43+
// fails for: (Bar1, Bar2)
44+
// fails for: (Bar1, Bar3)
45+
// fails for: (Bar2, Bar2)
46+
// fails for: (Bar2, Bar1)
3147
def fail4[T <: AnyRef](xx: (Foo[T], Foo[T])) = xx match {
3248
case (Bar1, Bar1) => ()
3349
case (Bar2, Bar3) => ()
3450
case (Bar3, _) => ()
3551
}
52+
// fails for: (Bar1, Bar2)
53+
// fails for: (Bar1, Bar3)
54+
// fails for: (Bar2, Bar1)
55+
// fails for: (Bar2, Bar2)
3656
def fail5[T](xx: (Foo[T], Foo[T])) = xx match {
3757
case (Bar1, Bar1) => ()
3858
case (Bar2, Bar3) => ()
3959
case (Bar3, _) => ()
4060
}
41-
42-
def main(args: Array[String]): Unit = {
43-
44-
}
4561
}

test/files/neg/pat_unreachable.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xoldpatmat
1+
-Xoldpatmat

test/files/neg/patmatexhaust.check

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,41 @@
1-
patmatexhaust.scala:7: error: match is not exhaustive!
2-
missing combination Baz
3-
1+
patmatexhaust.scala:7: error: match may not be exhaustive.
2+
It would fail on the following input: Baz
43
def ma1(x:Foo) = x match {
54
^
6-
patmatexhaust.scala:11: error: match is not exhaustive!
7-
missing combination Bar
8-
5+
patmatexhaust.scala:11: error: match may not be exhaustive.
6+
It would fail on the following input: Bar(_)
97
def ma2(x:Foo) = x match {
108
^
11-
patmatexhaust.scala:23: error: match is not exhaustive!
12-
missing combination Kult Kult
13-
missing combination Qult Qult
14-
9+
patmatexhaust.scala:23: error: match may not be exhaustive.
10+
It would fail on the following inputs: (Kult(_), Kult(_)), (Qult(), Qult())
1511
def ma3(x:Mult) = (x,x) match { // not exhaustive
1612
^
17-
patmatexhaust.scala:49: error: match is not exhaustive!
18-
missing combination Gp
19-
missing combination Gu
20-
13+
patmatexhaust.scala:49: error: match may not be exhaustive.
14+
It would fail on the following inputs: Gp(), Gu
2115
def ma4(x:Deep) = x match { // missing cases: Gu, Gp
2216
^
23-
patmatexhaust.scala:53: error: match is not exhaustive!
24-
missing combination Gp
25-
26-
def ma5(x:Deep) = x match { // Gp
17+
patmatexhaust.scala:53: error: match may not be exhaustive.
18+
It would fail on the following input: Gp()
19+
def ma5(x:Deep) = x match {
2720
^
28-
patmatexhaust.scala:59: error: match is not exhaustive!
29-
missing combination Nil
30-
21+
patmatexhaust.scala:59: error: match may not be exhaustive.
22+
It would fail on the following input: Nil
3123
def ma6() = List(1,2) match { // give up
3224
^
33-
patmatexhaust.scala:75: error: match is not exhaustive!
34-
missing combination B
35-
25+
patmatexhaust.scala:75: error: match may not be exhaustive.
26+
It would fail on the following input: B()
3627
def ma9(x: B) = x match {
3728
^
38-
patmatexhaust.scala:100: error: match is not exhaustive!
39-
missing combination C1
40-
29+
patmatexhaust.scala:100: error: match may not be exhaustive.
30+
It would fail on the following input: C1()
4131
def ma10(x: C) = x match { // not exhaustive: C1 is not sealed.
4232
^
43-
patmatexhaust.scala:114: error: match is not exhaustive!
44-
missing combination D1
45-
missing combination D2
46-
33+
patmatexhaust.scala:114: error: match may not be exhaustive.
34+
It would fail on the following inputs: D1, D2()
4735
def ma10(x: C) = x match { // not exhaustive: C1 has subclasses.
4836
^
49-
patmatexhaust.scala:126: error: match is not exhaustive!
50-
missing combination C1
51-
37+
patmatexhaust.scala:126: error: match may not be exhaustive.
38+
It would fail on the following input: C1()
5239
def ma10(x: C) = x match { // not exhaustive: C1 is not abstract.
5340
^
5441
10 errors found

test/files/neg/patmatexhaust.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xfatal-warnings -Xoldpatmat
1+
-Xfatal-warnings

test/files/neg/patmatexhaust.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class TestSealedExhaustive { // compile only
5050
case Ga =>
5151
}
5252

53-
def ma5(x:Deep) = x match { // Gp
53+
def ma5(x:Deep) = x match {
5454
case Gu =>
5555
case _ if 1 == 0 =>
5656
case Ga =>
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
sealed-java-enums.scala:5: error: match is not exhaustive!
2-
missing combination BLOCKED
3-
missing combination State
4-
missing combination TERMINATED
5-
missing combination TIMED_WAITING
6-
1+
sealed-java-enums.scala:5: error: match may not be exhaustive.
2+
It would fail on the following inputs: BLOCKED, TERMINATED, TIMED_WAITING
73
def f(state: State) = state match {
84
^
95
one error found
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xexperimental -Xfatal-warnings -Xoldpatmat
1+
-Xexperimental -Xfatal-warnings

test/files/neg/switch.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
switch.scala:28: error: could not emit switch for @switch annotated match
22
def fail1(c: Char) = (c: @switch) match {
3-
^
3+
^
44
switch.scala:38: error: could not emit switch for @switch annotated match
55
def fail2(c: Char) = (c: @switch @unchecked) match {
6-
^
6+
^
77
switch.scala:45: error: could not emit switch for @switch annotated match
88
def fail3(c: Char) = (c: @unchecked @switch) match {
9-
^
9+
^
1010
three errors found

test/files/neg/switch.flags

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/files/neg/t3098.check

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
b.scala:3: error: match is not exhaustive!
2-
missing combination C
3-
1+
b.scala:3: error: match may not be exhaustive.
2+
It would fail on the following input: (_ : C)
43
def f = (null: T) match {
54
^
65
one error found

test/files/neg/t3098.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xfatal-warnings -Xoldpatmat
1+
-Xfatal-warnings

test/files/neg/t3683a.check

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
t3683a.scala:14: error: match is not exhaustive!
2-
missing combination XX
3-
1+
t3683a.scala:14: error: match may not be exhaustive.
2+
It would fail on the following input: XX()
43
w match {
54
^
65
one error found

test/files/neg/t3683a.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xfatal-warnings -Xoldpatmat
1+
-Xfatal-warnings

test/files/neg/t3692-new.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xoldpatmat
1+
-Xoldpatmat

test/files/neg/t3692-old.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xoldpatmat
1+
-Xoldpatmat
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xfatal-warnings
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
sealed abstract class X
2+
sealed case class A(x: Boolean) extends X
3+
case object B extends X
4+
5+
object Test {
6+
def test(x: X) = x match {
7+
case A(true) =>
8+
case A(false) | B =>
9+
}
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// tests exhaustivity doesn't give warnings (due to its heuristic rewrites kicking in or it backing off)
2+
object Test {
3+
// List() => Nil
4+
List(1) match {
5+
case List() =>
6+
case x :: xs =>
7+
}
8+
9+
// we don't look into guards
10+
val turnOffChecks = true
11+
List(1) match {
12+
case _ if turnOffChecks =>
13+
}
14+
15+
// TODO: we back off when there are any user-defined extractors
16+
}

test/files/pos/t3097.scala

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)