Skip to content

Commit 74fb8f5

Browse files
committed
initial version without constant support
1 parent 134ad7a commit 74fb8f5

File tree

6 files changed

+348
-2
lines changed

6 files changed

+348
-2
lines changed

src/dotty/tools/dotc/core/Symbols.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,9 @@ object Symbols {
482482
}
483483
}
484484

485+
def children: Set[Symbol] = Set()
486+
def addChild(sym: Symbol): Unit = { throw new UnsupportedOperationException("addChild inapplicable for " + this) }
487+
485488
/** The position of this symbol, or NoPosition is symbol was not loaded
486489
* from source.
487490
*/
@@ -545,6 +548,12 @@ object Symbols {
545548
}
546549

547550
override protected def prefixString = "ClassSymbol"
551+
552+
// remember direct children of sealed classes
553+
private[this] var childSet: Set[Symbol] = Set()
554+
override def children = childSet
555+
def children_=(children: Set[Symbol]): Unit = childSet = children
556+
override def addChild(sym: Symbol): Unit = { childSet = childSet + sym }
548557
}
549558

550559
class ErrorSymbol(val underlying: Symbol, msg: => String)(implicit ctx: Context) extends Symbol(NoCoord, ctx.nextId) {

src/dotty/tools/dotc/core/classfile/ClassfileParser.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ class ClassfileParser(
223223
val enumClass = sym.owner.linkedClass
224224
if (!(enumClass is Flags.Sealed)) enumClass.setFlag(Flags.AbstractSealed)
225225
enumClass.addAnnotation(Annotation.makeChild(sym))
226+
enumClass.addChild(sym)
226227
}
227228
} finally {
228229
in.bp = oldbp

src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,11 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
800800
assert(tag == CHILDREN)
801801
val end = readNat() + readIndex
802802
val target = readSymbolRef()
803-
while (readIndex != end)
804-
target.addAnnotation(Annotation.makeChild(readSymbolRef()))
803+
while (readIndex != end) {
804+
val sym = readSymbolRef()
805+
target.addAnnotation(Annotation.makeChild(sym))
806+
target.addChild(sym)
807+
}
805808
}
806809

807810
/* Read a reference to a pickled item */

src/dotty/tools/dotc/transform/PatternMatcher.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,13 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
13041304
case _ => (cases, None)
13051305
}
13061306

1307+
// check exhaustivity and unreachability
1308+
val engine = new SpaceEngine
1309+
if (!engine.skipCheck(selectorTp)) {
1310+
engine.exhaustivity(Match(sel, nonSyntheticCases))
1311+
engine.redundancy(Match(sel, nonSyntheticCases))
1312+
}
1313+
13071314
// checkMatchVariablePatterns(nonSyntheticCases) // only used for warnings
13081315

13091316
// we don't transform after uncurry
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package dotty.tools.dotc
2+
package transform
3+
4+
import core.Types._
5+
import core.Contexts._
6+
import core.Flags
7+
import ast.Trees._
8+
import ast.tpd
9+
import core.Decorators._
10+
11+
12+
/** space logic for checking exhaustivity and unreachability of
13+
* pattern matching.
14+
*/
15+
16+
17+
// space definition
18+
sealed trait Space
19+
object Empty extends Space
20+
case class Typ(t: Type) extends Space
21+
case class Kon(t: Type, params: List[Space]) extends Space
22+
case class Or(spaces: List[Space]) extends Space
23+
24+
// abstract space logic
25+
trait SpaceLogic {
26+
def isSubType(t1: Type, t2: Type): Boolean
27+
def isEqualType(t1: Type, t2: Type): Boolean
28+
def isCaseClass(t: Type): Boolean
29+
30+
// Can a type be decompsed? i.e. all values of the type can be covered
31+
// by its decomposed types.
32+
//
33+
// abstract sealed class and OrType can be decomposed
34+
def canDecompose(t: Type): Boolean
35+
36+
// return parameters types of case class
37+
def getSignature(t: Type): List[Type]
38+
39+
// get components of decomposable types
40+
def getPartitions(t: Type): List[Type]
41+
42+
// simplify space using the laws, there's no nested union after simplify
43+
def simplify(space: Space): Space = space match {
44+
case Kon(t, spaces) =>
45+
val sp = Kon(t, spaces.map(simplify _))
46+
if (sp.params.exists(_ == Empty)) Empty
47+
else sp
48+
case Or(spaces) =>
49+
spaces.map(simplify _).flatMap {
50+
case Or(ss) => ss
51+
case s => Seq(s)
52+
} filter (_ != Empty) match {
53+
case ss @ x::y::xs => Or(ss)
54+
case s::Nil => s
55+
case Nil => Empty
56+
}
57+
case Typ(t) =>
58+
if (canDecompose(t) && getPartitions(t).isEmpty) Empty
59+
else space
60+
case _ => space
61+
}
62+
63+
// is a subspace of b, equivalent to `a - b == Empty`, but faster
64+
def subspace(a: Space, b: Space): Boolean = (a, b) match {
65+
case (Empty, _) => true
66+
case (_, Empty) => false
67+
case (Typ(t1), Typ(t2)) =>
68+
isSubType(t1, t2)
69+
case (Typ(t1), Or(ss)) =>
70+
ss.exists(subspace(a, _)) ||
71+
(canDecompose(t1) && subspace(Or(getPartitions(t1).map(Typ)), b))
72+
case (Typ(t1), Kon(t2, ss)) =>
73+
isSubType(t1, t2) && subspace(Kon(t2, getSignature(t2).map(Typ)), b)
74+
case (Or(ss), _) =>
75+
ss.forall(subspace(_, b))
76+
case (Kon(t1, ss), Typ(t2)) =>
77+
isSubType(t1, t2) ||
78+
simplify(a) == Empty ||
79+
(isSubType(t2, t1) &&
80+
canDecompose(t1) &&
81+
subspace(Or(getPartitions(t1).map(Typ)), b))
82+
case (Kon(t1, ss1), Or(ss2)) =>
83+
simplify(minus(a, b)) == Empty
84+
case (Kon(t1, ss1), Kon(t2, ss2)) =>
85+
isEqualType(t1, t2) && ss1.zip(ss2).forall((subspace _).tupled)
86+
}
87+
88+
// intersection of two spaces, non-commutative(inherit case class)
89+
def intersect(a: Space, b: Space): Space = (a, b) match {
90+
case (Empty, _) | (_, Empty) => Empty
91+
case (_, Or(ss)) => Or(ss.map(intersect(a, _)))
92+
case (Or(ss), _) => Or(ss.map(intersect(_, b)))
93+
case (Typ(t1), Typ(t2)) =>
94+
if (isSubType(t1, t2)) a
95+
else if (isSubType(t2, t1)) b
96+
else Empty
97+
case (Typ(t1), Kon(t2, ss)) =>
98+
if (isSubType(t2, t1)) b
99+
else if (isSubType(t1, t2)) a
100+
else Empty
101+
case (Kon(t1, ss), Typ(t2)) =>
102+
if (isSubType(t1, t2) || isSubType(t2, t1)) a
103+
else Empty
104+
case (Kon(t1, ss1), Kon(t2, ss2)) =>
105+
if (!isEqualType(t1, t2)) Empty
106+
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty
107+
else
108+
Kon(t1, ss1.zip(ss2).map((intersect _).tupled))
109+
}
110+
111+
// the space of a not covered by b
112+
def minus(a: Space, b: Space): Space = (a, b) match {
113+
case (Empty, _) => Empty
114+
case (_, Empty) => a
115+
case (Typ(t1), Typ(t2)) =>
116+
if (isSubType(t1, t2)) Empty
117+
else if (isSubType(t2, t1) && canDecompose(t1))
118+
minus(Or(getPartitions(t1).map(Typ)), b)
119+
else a
120+
case (Typ(t1), Kon(t2, ss)) =>
121+
if (isSubType(t1, t2)) minus(Kon(t2, getSignature(t2).map(Typ)), b)
122+
else if (isSubType(t2, t1) && canDecompose(t1))
123+
minus(Or(getPartitions(t1).map(Typ)), b)
124+
else a
125+
case (_, Or(ss)) =>
126+
ss.foldLeft(a)(minus)
127+
case (Or(ss), _) =>
128+
Or(ss.map(minus(_, b)))
129+
case (Kon(t1, ss), Typ(t2)) =>
130+
if (isSubType(t1, t2)) Empty
131+
else if (simplify(a) == Empty) Empty
132+
else if (isSubType(t2, t1) && canDecompose(t1))
133+
minus(Or(getPartitions(t1).map(Typ)), b)
134+
else a
135+
case (Kon(t1, ss1), Kon(t2, ss2)) =>
136+
if (!isEqualType(t1, t2)) a
137+
else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) a
138+
else if (ss1.zip(ss2).forall((subspace _).tupled)) Empty
139+
else
140+
Or(
141+
ss1.zip(ss2).map((minus _).tupled).zip(0 to ss2.length - 1).map {
142+
case (ri, i) => Kon(t1, ss1.updated(i, ri))
143+
})
144+
}
145+
}
146+
147+
// scala implementation of space logic
148+
class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
149+
def debug(s: String): Unit = {
150+
if (ctx.debug) println(s)
151+
}
152+
153+
// if roundUp, approximate extractors to its type
154+
// else approximate extractors to Empty
155+
def project(pat: tpd.Tree)(implicit ctx: Context, roundUp: Boolean = true): Space = pat match {
156+
case Ident(_) => Typ(pat.tpe)
157+
case Select(_, _) => Typ(pat.tpe)
158+
case Alternative(trees) => Or(trees.map(project _))
159+
case Bind(_, pat) => project(pat)
160+
case UnApply(_, _, pats) =>
161+
if (pat.tpe.classSymbol is Flags.CaseClass)
162+
Kon(pat.tpe, pats.map(project _))
163+
else if (roundUp) Typ(pat.tpe)
164+
else Empty
165+
case Typed(t@UnApply(_, _, _), _) => project(t)
166+
case Typed(expr, _) => Typ(expr.tpe)
167+
case _ =>
168+
debug(s"========unkown tree: $pat========")
169+
Empty
170+
}
171+
172+
def strip(t: Type)(implicit ctx: Context): Type = t match {
173+
case t: RefinedType => strip(t.underlying)
174+
case _ => t
175+
}
176+
177+
def isSubType(t1: Type, t2: Type): Boolean = {
178+
val res = t1 <:< t2
179+
debug(s"$t1 <: $t2 ? $res")
180+
res
181+
}
182+
183+
def isEqualType(t1: Type, t2: Type): Boolean = {
184+
val res = t1 =:= t2
185+
debug(s"$t1 == $t2 ? $res")
186+
res
187+
}
188+
189+
def getSignature(t: Type): List[Type] = {
190+
val ktor = t.classSymbol.primaryConstructor.info
191+
192+
def typeParams(t: Type): List[Type] = t match {
193+
case t: RefinedType => t.refinedInfo.asInstanceOf[TypeAlias].alias :: typeParams(t.parent)
194+
case _ => Nil
195+
}
196+
197+
val sign = (t match {
198+
case t: RefinedType => ktor.appliedTo(typeParams(t))
199+
case _ => ktor
200+
}).firstParamTypes.map(_.stripTypeVar)
201+
202+
debug(s"====signature of $t: $sign")
203+
sign
204+
}
205+
206+
def getPartitions(t: Type): List[Type] = t match {
207+
case OrType(t1, t2) => List(t1, t2)
208+
case _ =>
209+
val children = t.classSymbol.children.map { c =>
210+
if (c is Flags.ModuleClass)
211+
c.info.asInstanceOf[ClassInfo].selfType
212+
else
213+
specializeChild(t, c.info.asInstanceOf[ClassInfo].symbolicTypeRef)
214+
}.toList
215+
216+
debug(s"child of ${t.show}: ${children.map(_.show).mkString(", ")}")
217+
218+
children
219+
}
220+
221+
// parent is specialized, specialize child as well
222+
def specializeChild(p: Type, c: Type): Type = p match {
223+
case p: RefinedType => p.wrapIfMember(specializeChild(p.parent, c))
224+
case _ => c
225+
}
226+
227+
// abstract sealed types and or types can be decomposed
228+
def canDecompose(t: Type): Boolean = {
229+
t.typeSymbol.is(Flags.allOf(Flags.Abstract, Flags.Sealed)) || t.isInstanceOf[OrType]
230+
}
231+
232+
def isCaseClass(t: Type): Boolean = t.classSymbol.isClass && t.classSymbol.is(Flags.CaseClass)
233+
234+
// display spaces
235+
def show(s: Space): String = s match {
236+
case Empty => ""
237+
case Typ(t) =>
238+
val sym = t.widen.classSymbol
239+
240+
if (sym is Flags.ModuleClass)
241+
sym.sourceModule.name.show
242+
else if (t.isInstanceOf[OrType])
243+
"_: " + t.show
244+
else if (getSignature(t).isEmpty)
245+
"_: " + t.classSymbol.name
246+
else if (ctx.definitions.isTupleType(t))
247+
"(" + getSignature(t).map(_ => "_").mkString(", ") + ")"
248+
else
249+
t.classSymbol.name + "(" + getSignature(t).map(_ => "_").mkString(",") + ")"
250+
case Kon(t, params) =>
251+
if (ctx.definitions.isTupleType(t))
252+
"(" + params.map(show _).mkString(", ") + ")"
253+
else
254+
t.classSymbol.name + "(" + params.map(show _).mkString(", ") + ")"
255+
case Or(spaces) => spaces.map(show _).mkString(", ")
256+
}
257+
258+
// does the type t have @unchecked annotation
259+
def skipCheck(t: Type): Boolean = t match {
260+
case AnnotatedType(t, annot) => ctx.definitions.UncheckedAnnot == annot.symbol
261+
case _ => false
262+
}
263+
264+
// widen a type and eliminate anonymous class
265+
private def widen(t: Type): Type = t.widen match {
266+
case t:TypeRef if t.symbol.isAnonymousClass =>
267+
t.symbol.asClass.typeRef.asSeenFrom(t.prefix, t.symbol.owner)
268+
case t => t
269+
}
270+
271+
def exhaustivity(_match: tpd.Match): Unit = {
272+
val Match(sel, cases) = _match
273+
val selTyp = widen(sel.tpe)
274+
275+
debug(s"====patterns:\n${cases.map(_.pat).mkString("\n")}")
276+
debug(s"====pattern space:\n${show(cases.map(x => project(x.pat)).reduce((a, b) => Or(List(a, b))))}")
277+
val patternSpace = simplify(cases.map(x => project(x.pat)).reduce((a, b) => Or(List(a, b))))
278+
debug(s"====selector:\n" + selTyp)
279+
debug("====pattern space:\n" + show(patternSpace))
280+
val uncovered = simplify(minus(Typ(selTyp), patternSpace))
281+
282+
if (uncovered != Empty) {
283+
ctx.warning(
284+
"match may not be exhaustive.\n" +
285+
"It would fail on the following inputs: " +
286+
show(uncovered), _match.pos
287+
)
288+
}
289+
}
290+
291+
// find the first redundant pattern and stop
292+
def redundancy(_match: tpd.Match): Unit = {
293+
val Match(sel, cases) = _match
294+
val selTyp = widen(sel.tpe)
295+
296+
// starts from the second, the first can't be redundant
297+
var i = 1
298+
while(i < cases.length) {
299+
// in redundancy check, take guard as false, take extractor as match
300+
// nothing in order to soundly approximate
301+
val prevs = cases.take(i).map { x =>
302+
if (x.guard.isEmpty) project(x.pat)(ctx, false)
303+
else Empty
304+
}.reduce((a, b) => Or(List(a, b)))
305+
306+
val curr = project(cases(i).pat)
307+
308+
if (subspace(curr, prevs)) {
309+
ctx.warning("unreachable code", cases(i).body.pos)
310+
i = cases.length // break
311+
}
312+
313+
i += 1
314+
}
315+
}
316+
}

src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ trait NamerContextOps { this: Context =>
3333
case cls: ClassSymbol => cls.enter(sym)
3434
case _ => this.scope.openForMutations.enter(sym)
3535
}
36+
37+
// update child set of parent// update child set of parent
38+
sym match {
39+
case cls: ClassSymbol =>
40+
cls.classParents.map(_.classSymbol).filter(s => s.is(Flags.Sealed) && s != cls).foreach { p =>
41+
p.addChild(cls)
42+
}
43+
case _ =>
44+
}
45+
3646
sym
3747
}
3848

0 commit comments

Comments
 (0)