Skip to content

Commit 318700f

Browse files
authored
Merge pull request #2197 from dotty-staging/add-enum-exhaustiveness
Add enum exhaustivity checking
2 parents 80f9b6d + 2bf1e19 commit 318700f

14 files changed

+175
-105
lines changed

compiler/src/dotty/tools/dotc/config/Printers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ object Printers {
3131
val cyclicErrors: Printer = noPrinter
3232
val pickling: Printer = noPrinter
3333
val inlining: Printer = noPrinter
34+
val exhaustivity: Printer = noPrinter
3435
}

compiler/src/dotty/tools/dotc/transform/IsInstanceOfEvaluator.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer =>
147147
(scTrait && selTrait)
148148

149149
val inMatch = s.qualifier.symbol is Case
150+
// FIXME: This will misclassify case objects! We need to find another way to characterize
151+
// isInstanceOfs generated by matches.
152+
// Probably the most robust way is to use another symbol for the isInstanceOf method.
150153

151154
if (valueClassesOrAny) tree
152155
else if (knownStatically)

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,14 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
104104
private def transformAnnot(annot: Annotation)(implicit ctx: Context): Annotation =
105105
annot.derivedAnnotation(transformAnnot(annot.tree))
106106

107+
private def registerChild(sym: Symbol, tp: Type)(implicit ctx: Context) = {
108+
val cls = tp.classSymbol
109+
if (cls.is(Sealed)) cls.addAnnotation(Annotation.makeChild(sym))
110+
}
111+
107112
private def transformMemberDef(tree: MemberDef)(implicit ctx: Context): Unit = {
108113
val sym = tree.symbol
114+
if (sym.is(CaseVal, butNot = Method | Module)) registerChild(sym, sym.info)
109115
sym.transformAnnotations(transformAnnot)
110116
}
111117

@@ -227,9 +233,9 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisTran
227233

228234
// Add Child annotation to sealed parents unless current class is anonymous
229235
if (!sym.isAnonymousClass) // ignore anonymous class
230-
for (parent <- sym.asClass.classInfo.classParents) {
231-
val pclazz = parent.classSymbol
232-
if (pclazz.is(Sealed)) pclazz.addAnnotation(Annotation.makeChild(sym))
236+
sym.asClass.classInfo.classParents.foreach { parent =>
237+
val sym2 = if (sym.is(Module)) sym.sourceModule else sym
238+
registerChild(sym2, parent)
233239
}
234240

235241
tree

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

Lines changed: 64 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import core.StdNames._
1313
import core.NameOps._
1414
import core.Constants._
1515
import reporting.diagnostic.messages._
16+
import config.Printers.{ exhaustivity => debug }
1617

1718
/** Space logic for checking exhaustivity and unreachability of pattern matching
1819
*
@@ -28,8 +29,6 @@ import reporting.diagnostic.messages._
2829
* 3. A union of spaces `S1 | S2 | ...` is a space
2930
* 4. For a case class Kon(x1: T1, x2: T2, .., xn: Tn), if S1, S2, ..., Sn
3031
* are spaces, then `Kon(S1, S2, ..., Sn)` is a space.
31-
* 5. A constant `Const(value, T)` is a point in space
32-
* 6. A stable identifier `Var(sym, T)` is a space
3332
*
3433
* For the problem of exhaustivity check, its formulation in terms of space is as follows:
3534
*
@@ -67,15 +66,6 @@ case class Kon(tp: Type, params: List[Space]) extends Space
6766
/** Union of spaces */
6867
case class Or(spaces: List[Space]) extends Space
6968

70-
/** Point in space */
71-
sealed trait Point extends Space
72-
73-
/** Point representing variables(stable identifier) in patterns */
74-
case class Var(sym: Symbol, tp: Type) extends Point
75-
76-
/** Point representing literal constants in patterns */
77-
case class Const(value: Constant, tp: Type) extends Point
78-
7969
/** abstract space logic */
8070
trait SpaceLogic {
8171
/** Is `tp1` a subtype of `tp2`? */
@@ -97,6 +87,9 @@ trait SpaceLogic {
9787
/** Get components of decomposable types */
9888
def decompose(tp: Type): List[Space]
9989

90+
/** Display space in string format */
91+
def show(sp: Space): String
92+
10093
/** Simplify space using the laws, there's no nested union after simplify */
10194
def simplify(space: Space): Space = space match {
10295
case Kon(tp, spaces) =>
@@ -137,7 +130,7 @@ trait SpaceLogic {
137130
def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b)
138131
def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp)))
139132

140-
(a, b) match {
133+
val res = (a, b) match {
141134
case (Empty, _) => true
142135
case (_, Empty) => false
143136
case (Or(ss), _) => ss.forall(isSubspace(_, b))
@@ -157,25 +150,19 @@ trait SpaceLogic {
157150
simplify(minus(a, b)) == Empty
158151
case (Kon(tp1, ss1), Kon(tp2, ss2)) =>
159152
isEqualType(tp1, tp2) && ss1.zip(ss2).forall((isSubspace _).tupled)
160-
case (Const(v1, _), Const(v2, _)) => v1 == v2
161-
case (Const(_, tp1), Typ(tp2, _)) => isSubType(tp1, tp2) || tryDecompose2(tp2)
162-
case (Const(_, _), Or(ss)) => ss.exists(isSubspace(a, _))
163-
case (Const(_, _), _) => false
164-
case (_, Const(_, _)) => false
165-
case (Var(x, _), Var(y, _)) => x == y
166-
case (Var(_, tp1), Typ(tp2, _)) => isSubType(tp1, tp2) || tryDecompose2(tp2)
167-
case (Var(_, _), Or(ss)) => ss.exists(isSubspace(a, _))
168-
case (Var(_, _), _) => false
169-
case (_, Var(_, _)) => false
170153
}
154+
155+
debug.println(s"${show(a)} < ${show(b)} = $res")
156+
157+
res
171158
}
172159

173160
/** Intersection of two spaces */
174161
def intersect(a: Space, b: Space): Space = {
175162
def tryDecompose1(tp: Type) = intersect(Or(decompose(tp)), b)
176163
def tryDecompose2(tp: Type) = intersect(a, Or(decompose(tp)))
177164

178-
(a, b) match {
165+
val res = (a, b) match {
179166
case (Empty, _) | (_, Empty) => Empty
180167
case (_, Or(ss)) => Or(ss.map(intersect(a, _)).filterConserve(_ ne Empty))
181168
case (Or(ss), _) => Or(ss.map(intersect(_, b)).filterConserve(_ ne Empty))
@@ -199,39 +186,19 @@ trait SpaceLogic {
199186
if (!isEqualType(tp1, tp2)) Empty
200187
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
201188
else Kon(tp1, ss1.zip(ss2).map((intersect _).tupled))
202-
case (Const(v1, _), Const(v2, _)) =>
203-
if (v1 == v2) a else Empty
204-
case (Const(_, tp1), Typ(tp2, _)) =>
205-
if (isSubType(tp1, tp2)) a
206-
else if (canDecompose(tp2)) tryDecompose2(tp2)
207-
else Empty
208-
case (Const(_, _), _) => Empty
209-
case (Typ(tp1, _), Const(_, tp2)) =>
210-
if (isSubType(tp2, tp1)) b
211-
else if (canDecompose(tp1)) tryDecompose1(tp1)
212-
else Empty
213-
case (_, Const(_, _)) => Empty
214-
case (Var(x, _), Var(y, _)) =>
215-
if (x == y) a else Empty
216-
case (Var(_, tp1), Typ(tp2, _)) =>
217-
if (isSubType(tp1, tp2)) a
218-
else if (canDecompose(tp2)) tryDecompose2(tp2)
219-
else Empty
220-
case (Var(_, _), _) => Empty
221-
case (Typ(tp1, _), Var(_, tp2)) =>
222-
if (isSubType(tp2, tp1)) b
223-
else if (canDecompose(tp1)) tryDecompose1(tp1)
224-
else Empty
225-
case (_, Var(_, _)) => Empty
226189
}
190+
191+
debug.println(s"${show(a)} & ${show(b)} = ${show(res)}")
192+
193+
res
227194
}
228195

229196
/** The space of a not covered by b */
230197
def minus(a: Space, b: Space): Space = {
231198
def tryDecompose1(tp: Type) = minus(Or(decompose(tp)), b)
232199
def tryDecompose2(tp: Type) = minus(a, Or(decompose(tp)))
233200

234-
(a, b) match {
201+
val res = (a, b) match {
235202
case (Empty, _) => Empty
236203
case (_, Empty) => a
237204
case (Typ(tp1, _), Typ(tp2, _)) =>
@@ -264,26 +231,11 @@ trait SpaceLogic {
264231
Or(ss1.zip(ss2).map((minus _).tupled).zip(0 to ss2.length - 1).map {
265232
case (ri, i) => Kon(tp1, ss1.updated(i, ri))
266233
})
267-
case (Const(v1, _), Const(v2, _)) =>
268-
if (v1 == v2) Empty else a
269-
case (Const(_, tp1), Typ(tp2, _)) =>
270-
if (isSubType(tp1, tp2)) Empty
271-
else if (canDecompose(tp2)) tryDecompose2(tp2)
272-
else a
273-
case (Const(_, _), _) => a
274-
case (Typ(tp1, _), Const(_, tp2)) => // Boolean & Java enum
275-
if (canDecompose(tp1)) tryDecompose1(tp1)
276-
else a
277-
case (_, Const(_, _)) => a
278-
case (Var(x, _), Var(y, _)) =>
279-
if (x == y) Empty else a
280-
case (Var(_, tp1), Typ(tp2, _)) =>
281-
if (isSubType(tp1, tp2)) Empty
282-
else if (canDecompose(tp2)) tryDecompose2(tp2)
283-
else a
284-
case (Var(_, _), _) => a
285-
case (_, Var(_, _)) => a
286234
}
235+
236+
debug.println(s"${show(a)} - ${show(b)} = ${show(res)}")
237+
238+
res
287239
}
288240
}
289241

@@ -297,19 +249,14 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
297249
* otherwise approximate extractors to Empty
298250
*/
299251
def project(pat: Tree, roundUp: Boolean = true)(implicit ctx: Context): Space = pat match {
300-
case Literal(c) => Const(c, c.tpe)
301-
case _: BackquotedIdent => Var(pat.symbol, pat.tpe)
252+
case Literal(c) =>
253+
if (c.value.isInstanceOf[Symbol])
254+
Typ(c.value.asInstanceOf[Symbol].termRef, false)
255+
else
256+
Typ(ConstantType(c), false)
257+
case _: BackquotedIdent => Typ(pat.tpe, false)
302258
case Ident(_) | Select(_, _) =>
303-
pat.tpe.stripAnnots match {
304-
case tp: TermRef =>
305-
if (pat.symbol.is(Enum))
306-
Const(Constant(pat.symbol), tp)
307-
else if (tp.underlyingIterator.exists(_.classSymbol.is(Module)))
308-
Typ(tp.widenTermRefExpr.stripAnnots, false)
309-
else
310-
Var(pat.symbol, tp)
311-
case tp => Typ(tp, false)
312-
}
259+
Typ(pat.tpe.stripAnnots, false)
313260
case Alternative(trees) => Or(trees.map(project(_, roundUp)))
314261
case Bind(_, pat) => project(pat)
315262
case UnApply(_, _, pats) =>
@@ -345,7 +292,9 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
345292
/** Is `tp1` a subtype of `tp2`? */
346293
def isSubType(tp1: Type, tp2: Type): Boolean = {
347294
// check SI-9657 and tests/patmat/gadt.scala
348-
erase(tp1) <:< erase(tp2)
295+
val res = erase(tp1) <:< erase(tp2)
296+
debug.println(s"${tp1.show} <:< ${tp2.show} = $res")
297+
res
349298
}
350299

351300
def isEqualType(tp1: Type, tp2: Type): Boolean = tp1 =:= tp2
@@ -373,29 +322,37 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
373322
}
374323
}
375324

325+
debug.println(s"candidates for ${tp.show} : [${children.map(_.show).mkString(", ")}]")
326+
376327
tp match {
377328
case OrType(tp1, tp2) => List(Typ(tp1, true), Typ(tp2, true))
378329
case _ if tp =:= ctx.definitions.BooleanType =>
379330
List(
380-
Const(Constant(true), ctx.definitions.BooleanType),
381-
Const(Constant(false), ctx.definitions.BooleanType)
331+
Typ(ConstantType(Constant(true)), true),
332+
Typ(ConstantType(Constant(false)), true)
382333
)
383334
case _ if tp.classSymbol.is(Enum) =>
384-
children.map(sym => Const(Constant(sym), tp))
335+
children.map(sym => Typ(sym.termRef, true))
385336
case _ =>
386337
val parts = children.map { sym =>
387338
if (sym.is(ModuleClass))
388-
sym.asClass.classInfo.selfType
339+
refine(tp, sym.sourceModule.termRef)
340+
else if (sym.isTerm)
341+
refine(tp, sym.termRef)
389342
else if (sym.info.typeParams.length > 0 || tp.isInstanceOf[TypeRef])
390343
refine(tp, sym.typeRef)
391344
else
392345
sym.typeRef
393346
} filter { tpe =>
394347
// Child class may not always be subtype of parent:
395348
// GADT & path-dependent types
396-
tpe <:< expose(tp)
349+
val res = tpe <:< expose(tp)
350+
if (!res) debug.println(s"unqualified child ousted: ${tpe.show} !< ${tp.show}")
351+
res
397352
}
398353

354+
debug.println(s"${tp.show} decomposes to [${parts.map(_.show).mkString(", ")}]")
355+
399356
parts.map(Typ(_, true))
400357
}
401358
}
@@ -409,20 +366,26 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
409366
* `path2`, then return `path1.B`.
410367
*/
411368
def refine(tp1: Type, tp2: Type): Type = (tp1, tp2) match {
412-
case (tp1: RefinedType, _) => tp1.wrapIfMember(refine(tp1.parent, tp2))
369+
case (tp1: RefinedType, _: TypeRef) => tp1.wrapIfMember(refine(tp1.parent, tp2))
413370
case (tp1: HKApply, _) => refine(tp1.superType, tp2)
414-
case (TypeRef(ref1: TypeProxy, _), tp2 @ TypeRef(ref2: TypeProxy, name)) =>
415-
if (ref1.underlying <:< ref2.underlying) TypeRef(ref1, name) else tp2
371+
case (TypeRef(ref1: TypeProxy, _), tp2 @ TypeRef(ref2: TypeProxy, _)) =>
372+
if (ref1.underlying <:< ref2.underlying) tp2.derivedSelect(ref1) else tp2
373+
case (TypeRef(ref1: TypeProxy, _), tp2 @ TermRef(ref2: TypeProxy, _)) =>
374+
if (ref1.underlying <:< ref2.underlying) tp2.derivedSelect(ref1) else tp2
416375
case _ => tp2
417376
}
418377

419378
/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */
420379
def canDecompose(tp: Type): Boolean = {
421-
tp.classSymbol.is(allOf(Abstract, Sealed)) ||
380+
val res = tp.classSymbol.is(allOf(Abstract, Sealed)) ||
422381
tp.classSymbol.is(allOf(Trait, Sealed)) ||
423382
tp.isInstanceOf[OrType] ||
424383
tp =:= ctx.definitions.BooleanType ||
425-
tp.classSymbol.is(Enum)
384+
tp.classSymbol.is(allOf(Enum, Sealed)) // Enum value doesn't have Sealed flag
385+
386+
debug.println(s"decomposable: ${tp.show} = $res")
387+
388+
res
426389
}
427390

428391
/** Show friendly type name with current scope in mind
@@ -474,14 +437,12 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
474437
def show(s: Space): String = {
475438
def doShow(s: Space, mergeList: Boolean = false): String = s match {
476439
case Empty => ""
477-
case Const(v, _) => v.show
478-
case Var(x, _) => x.show
440+
case Typ(c: ConstantType, _) => c.value.show
441+
case Typ(tp: TermRef, _) => tp.symbol.showName
479442
case Typ(tp, decomposed) =>
480443
val sym = tp.widen.classSymbol
481444

482-
if (sym.is(ModuleClass))
483-
showType(tp)
484-
else if (ctx.definitions.isTupleType(tp))
445+
if (ctx.definitions.isTupleType(tp))
485446
signature(tp).map(_ => "_").mkString("(", ", ", ")")
486447
else if (sym.showFullName == "scala.collection.immutable.::")
487448
if (mergeList) "_" else "List(_)"
@@ -523,7 +484,9 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
523484
}
524485

525486
val Match(sel, cases) = tree
526-
isCheckable(sel.tpe.widen.deAnonymize.dealiasKeepAnnots)
487+
val res = isCheckable(sel.tpe.widen.deAnonymize.dealiasKeepAnnots)
488+
debug.println(s"checkable: ${sel.show} = $res")
489+
res
527490
}
528491

529492

@@ -584,7 +547,11 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
584547
val selTyp = sel.tpe.widen.deAnonymize.dealias
585548

586549

587-
val patternSpace = cases.map(x => project(x.pat)).reduce((a, b) => Or(List(a, b)))
550+
val patternSpace = cases.map({ x =>
551+
val space = project(x.pat)
552+
debug.println(s"${x.pat.show} projects to ${show(space)}")
553+
space
554+
}).reduce((a, b) => Or(List(a, b)))
588555
val uncovered = simplify(minus(Typ(selTyp, true), patternSpace))
589556

590557
if (uncovered != Empty)

tests/patmat/enum-HList.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
enum HLst {
2+
case HCons[+Hd, +Tl <: HLst](hd: Hd, tl: Tl)
3+
case HNil
4+
}
5+
6+
object Test {
7+
import HLst._
8+
def length(hl: HLst): Int = hl match {
9+
case HCons(_, tl) => 1 + length(tl)
10+
case HNil => 0
11+
}
12+
def sumInts(hl: HLst): Int = hl match {
13+
case HCons(x: Int, tl) => x + sumInts(tl)
14+
case HCons(_, tl) => sumInts(tl)
15+
case HNil => 0
16+
}
17+
def main(args: Array[String]) = {
18+
val hl = HCons(1, HCons("A", HNil))
19+
assert(length(hl) == 2, length(hl))
20+
assert(sumInts(hl) == 1)
21+
}
22+
}

0 commit comments

Comments
 (0)