Skip to content

Commit 8d25e84

Browse files
committed
Merge pull request scala#3954 from gbasler/ticket/7746-2.11
SI-7746 fix unspecifc non-exhaustiveness warnings and non-determinism in pattern matcher (2.11)
2 parents 830425a + 34b42d8 commit 8d25e84

File tree

11 files changed

+129
-63
lines changed

11 files changed

+129
-63
lines changed

src/compiler/scala/tools/nsc/settings/ScalaSettings.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ trait ScalaSettings extends AbsScalaSettings
262262
val Yreifydebug = BooleanSetting("-Yreify-debug", "Trace reification.")
263263
val Ytyperdebug = BooleanSetting("-Ytyper-debug", "Trace all type assignments.")
264264
val Ypatmatdebug = BooleanSetting("-Ypatmat-debug", "Trace pattern matching translation.")
265+
val YpatmatExhaustdepth = IntSetting("-Ypatmat-exhaust-depth", "off", 20, Some((10, Int.MaxValue)),
266+
str => Some(if(str.equalsIgnoreCase("off")) Int.MaxValue else str.toInt))
265267
val Yquasiquotedebug = BooleanSetting("-Yquasiquote-debug", "Trace quasiquote-related activities.")
266268

267269
// TODO 2.12 Remove

src/compiler/scala/tools/nsc/transform/patmat/Logic.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package tools.nsc.transform.patmat
1010
import scala.language.postfixOps
1111
import scala.collection.mutable
1212
import scala.reflect.internal.util.Statistics
13-
import scala.reflect.internal.util.Position
1413
import scala.reflect.internal.util.HashSet
1514

1615
trait Logic extends Debugging {
@@ -72,6 +71,8 @@ trait Logic extends Debugging {
7271
def unapply(v: Var): Some[Tree]
7372
}
7473

74+
def reportWarning(message: String): Unit
75+
7576
// resets hash consing -- only supposed to be called by TreeMakersToProps
7677
def prepareNewAnalysis(): Unit
7778

@@ -86,7 +87,7 @@ trait Logic extends Debugging {
8687
def mayBeNull: Boolean
8788

8889
// compute the domain and return it (call registerNull first!)
89-
def domainSyms: Option[mutable.LinkedHashSet[Sym]]
90+
def domainSyms: Option[Set[Sym]]
9091

9192
// the symbol for this variable being equal to its statically known type
9293
// (only available if registerEquality has been called for that type before)
@@ -204,7 +205,7 @@ trait Logic extends Debugging {
204205
def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Formula, List[Formula]) = {
205206
val start = if (Statistics.canEnable) Statistics.startTimer(patmatAnaVarEq) else null
206207

207-
val vars = mutable.LinkedHashSet[Var]()
208+
val vars = new mutable.HashSet[Var]
208209

209210
object gatherEqualities extends PropTraverser {
210211
override def apply(p: Prop) = p match {
@@ -291,7 +292,7 @@ trait Logic extends Debugging {
291292
def eqFreePropToSolvable(p: Prop): Formula
292293
def cnfString(f: Formula): String
293294

294-
type Model = collection.immutable.SortedMap[Sym, Boolean]
295+
type Model = Map[Sym, Boolean]
295296
val EmptyModel: Model
296297
val NoModel: Model
297298

@@ -341,9 +342,9 @@ trait ScalaLogic extends Interface with Logic with TreeAndTypeAnalysis {
341342
// we enumerate the subtypes of the full type, as that allows us to filter out more types statically,
342343
// once we go to run-time checks (on Const's), convert them to checkable types
343344
// TODO: there seems to be bug for singleton domains (variable does not show up in model)
344-
lazy val domain: Option[mutable.LinkedHashSet[Const]] = {
345-
val subConsts: Option[mutable.LinkedHashSet[Const]] = enumerateSubtypes(staticTp).map { tps =>
346-
mutable.LinkedHashSet(tps: _*).map{ tp =>
345+
lazy val domain: Option[Set[Const]] = {
346+
val subConsts = enumerateSubtypes(staticTp).map{ tps =>
347+
tps.toSet[Type].map{ tp =>
347348
val domainC = TypeConst(tp)
348349
registerEquality(domainC)
349350
domainC
@@ -486,7 +487,7 @@ trait ScalaLogic extends Interface with Logic with TreeAndTypeAnalysis {
486487
}
487488

488489
// accessing after calling registerNull will result in inconsistencies
489-
lazy val domainSyms: Option[collection.mutable.LinkedHashSet[Sym]] = domain map { _ map symForEqualsTo }
490+
lazy val domainSyms: Option[Set[Sym]] = domain map { _ map symForEqualsTo }
490491

491492
lazy val symForStaticTp: Option[Sym] = symForEqualsTo.get(TypeConst(staticTpCheckable))
492493

src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package scala.tools.nsc.transform.patmat
99
import scala.language.postfixOps
1010
import scala.collection.mutable
1111
import scala.reflect.internal.util.Statistics
12-
import scala.reflect.internal.util.Position
1312

1413
trait TreeAndTypeAnalysis extends Debugging {
1514
import global._
@@ -400,6 +399,7 @@ trait MatchAnalysis extends MatchApproximation {
400399
trait MatchAnalyzer extends MatchApproximator {
401400
def uncheckedWarning(pos: Position, msg: String) = currentRun.reporting.uncheckedWarning(pos, msg)
402401
def warn(pos: Position, ex: AnalysisBudget.Exception, kind: String) = uncheckedWarning(pos, s"Cannot check match for $kind.\n${ex.advice}")
402+
def reportWarning(message: String) = global.reporter.warning(typer.context.tree.pos, message)
403403

404404
// TODO: model dependencies between variables: if V1 corresponds to (x: List[_]) and V2 is (x.hd), V2 cannot be assigned when V1 = null or V1 = Nil
405405
// right now hackily implement this by pruning counter-examples
@@ -524,9 +524,11 @@ trait MatchAnalysis extends MatchApproximation {
524524
val matchFailModels = findAllModelsFor(propToSolvable(matchFails))
525525

526526
val scrutVar = Var(prevBinderTree)
527-
val counterExamples = matchFailModels.map(modelToCounterExample(scrutVar))
528-
529-
val pruned = CounterExample.prune(counterExamples).map(_.toString).sorted
527+
val counterExamples = matchFailModels.flatMap(modelToCounterExample(scrutVar))
528+
// sorting before pruning is important here in order to
529+
// keep neg/t7020.scala stable
530+
// since e.g. List(_, _) would cover List(1, _)
531+
val pruned = CounterExample.prune(counterExamples.sortBy(_.toString)).map(_.toString)
530532

531533
if (Statistics.canEnable) Statistics.stopTimer(patmatAnaExhaust, start)
532534
pruned
@@ -616,7 +618,7 @@ trait MatchAnalysis extends MatchApproximation {
616618
// (the variables don't take into account type information derived from other variables,
617619
// so, naively, you might try to construct a counter example like _ :: Nil(_ :: _, _ :: _),
618620
// since we didn't realize the tail of the outer cons was a Nil)
619-
def modelToCounterExample(scrutVar: Var)(model: Model): CounterExample = {
621+
def modelToCounterExample(scrutVar: Var)(model: Model): Option[CounterExample] = {
620622
// x1 = ...
621623
// x1.hd = ...
622624
// x1.tl = ...
@@ -674,6 +676,7 @@ trait MatchAnalysis extends MatchApproximation {
674676
private val fields: mutable.Map[Symbol, VariableAssignment] = mutable.HashMap.empty
675677
// need to prune since the model now incorporates all super types of a constant (needed for reachability)
676678
private lazy val uniqueEqualTo = equalTo filterNot (subsumed => equalTo.exists(better => (better ne subsumed) && instanceOfTpImplies(better.tp, subsumed.tp)))
679+
private lazy val inSameDomain = uniqueEqualTo forall (const => variable.domainSyms.exists(_.exists(_.const.tp =:= const.tp)))
677680
private lazy val prunedEqualTo = uniqueEqualTo filterNot (subsumed => variable.staticTpCheckable <:< subsumed.tp)
678681
private lazy val ctor = (prunedEqualTo match { case List(TypeConst(tp)) => tp case _ => variable.staticTpCheckable }).typeSymbol.primaryConstructor
679682
private lazy val ctorParams = if (ctor.paramss.isEmpty) Nil else ctor.paramss.head
@@ -694,13 +697,13 @@ trait MatchAnalysis extends MatchApproximation {
694697
// NoExample if the constructor call is ill-typed
695698
// (thus statically impossible -- can we incorporate this into the formula?)
696699
// beBrief is used to suppress negative information nested in tuples -- it tends to get too noisy
697-
def toCounterExample(beBrief: Boolean = false): CounterExample =
698-
if (!allFieldAssignmentsLegal) NoExample
700+
def toCounterExample(beBrief: Boolean = false): Option[CounterExample] =
701+
if (!allFieldAssignmentsLegal) Some(NoExample)
699702
else {
700703
debug.patmat("describing "+ ((variable, equalTo, notEqualTo, fields, cls, allFieldAssignmentsLegal)))
701704
val res = prunedEqualTo match {
702705
// a definite assignment to a value
703-
case List(eq: ValueConst) if fields.isEmpty => ValueExample(eq)
706+
case List(eq: ValueConst) if fields.isEmpty => Some(ValueExample(eq))
704707

705708
// constructor call
706709
// or we did not gather any information about equality but we have information about the fields
@@ -713,30 +716,50 @@ trait MatchAnalysis extends MatchApproximation {
713716
// figure out the constructor arguments from the field assignment
714717
val argLen = (caseFieldAccs.length min ctorParams.length)
715718

716-
(0 until argLen).map(i => fields.get(caseFieldAccs(i)).map(_.toCounterExample(brevity)) getOrElse WildcardExample).toList
719+
val examples = (0 until argLen).map(i => fields.get(caseFieldAccs(i)).map(_.toCounterExample(brevity)) getOrElse Some(WildcardExample)).toList
720+
sequence(examples)
717721
}
718722

719723
cls match {
720-
case ConsClass => ListExample(args())
721-
case _ if isTupleSymbol(cls) => TupleExample(args(brevity = true))
722-
case _ => ConstructorExample(cls, args())
724+
case ConsClass =>
725+
args().map {
726+
case List(NoExample, l: ListExample) =>
727+
// special case for neg/t7020.scala:
728+
// if we find a counter example `??::*` we report `*::*` instead
729+
// since the `??` originates from uniqueEqualTo containing several instanced of the same type
730+
List(WildcardExample, l)
731+
case args => args
732+
}.map(ListExample)
733+
case _ if isTupleSymbol(cls) => args(brevity = true).map(TupleExample)
734+
case _ if cls.isSealed && cls.isAbstractClass =>
735+
// don't report sealed abstract classes, since
736+
// 1) they can't be instantiated
737+
// 2) we are already reporting any missing subclass (since we know the full domain)
738+
// (see patmatexhaust.scala)
739+
None
740+
case _ => args().map(ConstructorExample(cls, _))
723741
}
724742

725743
// a definite assignment to a type
726-
case List(eq) if fields.isEmpty => TypeExample(eq)
744+
case List(eq) if fields.isEmpty => Some(TypeExample(eq))
727745

728746
// negative information
729747
case Nil if nonTrivialNonEqualTo.nonEmpty =>
730748
// negation tends to get pretty verbose
731-
if (beBrief) WildcardExample
749+
if (beBrief) Some(WildcardExample)
732750
else {
733751
val eqTo = equalTo.headOption getOrElse TypeConst(variable.staticTpCheckable)
734-
NegativeExample(eqTo, nonTrivialNonEqualTo)
752+
Some(NegativeExample(eqTo, nonTrivialNonEqualTo))
735753
}
736754

755+
// if uniqueEqualTo contains more than one symbol of the same domain
756+
// then we can safely ignore these counter examples since we will eventually encounter
757+
// both counter examples separately
758+
case _ if inSameDomain => None
759+
737760
// not a valid counter-example, possibly since we have a definite type but there was a field mismatch
738761
// TODO: improve reasoning -- in the mean time, a false negative is better than an annoying false positive
739-
case _ => NoExample
762+
case _ => Some(NoExample)
740763
}
741764
debug.patmatResult("described as")(res)
742765
}

src/compiler/scala/tools/nsc/transform/patmat/Solving.scala

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,9 @@ trait Solving extends Logic {
2626
type Formula = FormulaBuilder
2727
def formula(c: Clause*): Formula = ArrayBuffer(c: _*)
2828

29-
type Clause = collection.Set[Lit]
29+
type Clause = Set[Lit]
3030
// a clause is a disjunction of distinct literals
31-
def clause(l: Lit*): Clause = (
32-
if (l.lengthCompare(1) <= 0) {
33-
l.toSet // SI-8531 Avoid LinkedHashSet's bulk for 0 and 1 element clauses
34-
} else {
35-
// neg/t7020.scala changes output 1% of the time, the non-determinism is quelled with this linked set
36-
mutable.LinkedHashSet(l: _*)
37-
}
38-
)
31+
def clause(l: Lit*): Clause = l.toSet
3932

4033
type Lit
4134
def Lit(sym: Sym, pos: Boolean = true): Lit
@@ -115,7 +108,6 @@ trait Solving extends Logic {
115108

116109
if (Statistics.canEnable) Statistics.stopTimer(patmatCNF, start)
117110

118-
//
119111
if (Statistics.canEnable) patmatCNFSizes(res.size).value += 1
120112

121113
// debug.patmat("cnf for\n"+ p +"\nis:\n"+cnfString(res))
@@ -141,36 +133,83 @@ trait Solving extends Logic {
141133
def cnfString(f: Formula) = alignAcrossRows(f map (_.toList) toList, "\\/", " /\\\n")
142134

143135
// adapted from http://lara.epfl.ch/w/sav10:simple_sat_solver (original by Hossein Hojjat)
144-
val EmptyModel = collection.immutable.SortedMap.empty[Sym, Boolean]
136+
val EmptyModel = Map.empty[Sym, Boolean]
145137
val NoModel: Model = null
146138

147139
// returns all solutions, if any (TODO: better infinite recursion backstop -- detect fixpoint??)
148140
def findAllModelsFor(f: Formula): List[Model] = {
141+
142+
debug.patmat("find all models for\n"+ cnfString(f))
143+
149144
val vars: Set[Sym] = f.flatMap(_ collect {case l: Lit => l.sym}).toSet
150145
// debug.patmat("vars "+ vars)
151146
// the negation of a model -(S1=True/False /\ ... /\ SN=True/False) = clause(S1=False/True, ...., SN=False/True)
152147
def negateModel(m: Model) = clause(m.toSeq.map{ case (sym, pos) => Lit(sym, !pos) } : _*)
153148

154-
def findAllModels(f: Formula, models: List[Model], recursionDepthAllowed: Int = 10): List[Model]=
155-
if (recursionDepthAllowed == 0) models
156-
else {
157-
debug.patmat("find all models for\n"+ cnfString(f))
149+
/**
150+
* The DPLL procedure only returns a minimal mapping from literal to value
151+
* such that the CNF formula is satisfied.
152+
* E.g. for:
153+
* `(a \/ b)`
154+
* The DPLL procedure will find either {a = true} or {b = true}
155+
* as solution.
156+
*
157+
* The expansion step will amend both solutions with the unassigned variable
158+
* i.e., {a = true} will be expanded to {a = true, b = true} and {a = true, b = false}.
159+
*/
160+
def expandUnassigned(unassigned: List[Sym], model: Model): List[Model] = {
161+
// the number of solutions is doubled for every unassigned variable
162+
val expandedModels = 1 << unassigned.size
163+
var current = mutable.ArrayBuffer[Model]()
164+
var next = mutable.ArrayBuffer[Model]()
165+
current.sizeHint(expandedModels)
166+
next.sizeHint(expandedModels)
167+
168+
current += model
169+
170+
// we use double buffering:
171+
// read from `current` and create a two models for each model in `next`
172+
for {
173+
s <- unassigned
174+
} {
175+
for {
176+
model <- current
177+
} {
178+
def force(l: Lit) = model + (l.sym -> l.pos)
179+
180+
next += force(Lit(s, pos = true))
181+
next += force(Lit(s, pos = false))
182+
}
183+
184+
val tmp = current
185+
current = next
186+
next = tmp
187+
188+
next.clear()
189+
}
190+
191+
current.toList
192+
}
193+
194+
def findAllModels(f: Formula,
195+
models: List[Model],
196+
recursionDepthAllowed: Int = global.settings.YpatmatExhaustdepth.value): List[Model]=
197+
if (recursionDepthAllowed == 0) {
198+
val maxDPLLdepth = global.settings.YpatmatExhaustdepth.value
199+
reportWarning("(Exhaustivity analysis reached max recursion depth, not all missing cases are reported. " +
200+
s"Please try with scalac -Ypatmat-exhaust-depth ${maxDPLLdepth * 2} or -Ypatmat-exhaust-depth off.)")
201+
models
202+
} else {
158203
val model = findModelFor(f)
159204
// if we found a solution, conjunct the formula with the model's negation and recurse
160205
if (model ne NoModel) {
161206
val unassigned = (vars -- model.keySet).toList
162207
debug.patmat("unassigned "+ unassigned +" in "+ model)
163-
def force(lit: Lit) = {
164-
val model = withLit(findModelFor(dropUnit(f, lit)), lit)
165-
if (model ne NoModel) List(model)
166-
else Nil
167-
}
168-
val forced = unassigned flatMap { s =>
169-
force(Lit(s, pos = true)) ++ force(Lit(s, pos = false))
170-
}
208+
209+
val forced = expandUnassigned(unassigned, model)
171210
debug.patmat("forced "+ forced)
172211
val negated = negateModel(model)
173-
findAllModels(f :+ negated, model :: (forced ++ models), recursionDepthAllowed - 1)
212+
findAllModels(f :+ negated, forced ++ models, recursionDepthAllowed - 1)
174213
}
175214
else models
176215
}
@@ -210,15 +249,14 @@ trait Solving extends Logic {
210249
withLit(findModelFor(dropUnit(f, unitLit)), unitLit)
211250
case _ =>
212251
// partition symbols according to whether they appear in positive and/or negative literals
213-
// SI-7020 Linked- for deterministic counter examples.
214-
val pos = new mutable.LinkedHashSet[Sym]()
215-
val neg = new mutable.LinkedHashSet[Sym]()
252+
val pos = new mutable.HashSet[Sym]()
253+
val neg = new mutable.HashSet[Sym]()
216254
mforeach(f)(lit => if (lit.pos) pos += lit.sym else neg += lit.sym)
217255

218256
// appearing in both positive and negative
219-
val impures: mutable.LinkedHashSet[Sym] = pos intersect neg
257+
val impures = pos intersect neg
220258
// appearing only in either positive/negative positions
221-
val pures: mutable.LinkedHashSet[Sym] = (pos ++ neg) -- impures
259+
val pures = (pos ++ neg) -- impures
222260

223261
if (pures nonEmpty) {
224262
val pureSym = pures.head

test/files/neg/patmatexhaust.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ It would fail on the following inputs: (Kult(_), Kult(_)), (Qult(), Qult())
1212
^
1313
patmatexhaust.scala:49: warning: match may not be exhaustive.
1414
It would fail on the following inputs: Gp(), Gu
15-
def ma4(x:Deep) = x match { // missing cases: Gu, Gp
15+
def ma4(x:Deep) = x match { // missing cases: Gu, Gp which is not abstract so must be included
1616
^
1717
patmatexhaust.scala:55: warning: unreachable code
1818
case _ if 1 == 0 =>

test/files/neg/patmatexhaust.flags

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-Xfatal-warnings
1+
-Xfatal-warnings -Ypatmat-exhaust-depth off

test/files/neg/patmatexhaust.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class TestSealedExhaustive { // compile only
4646
case _ =>
4747
}
4848

49-
def ma4(x:Deep) = x match { // missing cases: Gu, Gp
49+
def ma4(x:Deep) = x match { // missing cases: Gu, Gp which is not abstract so must be included
5050
case Ga =>
5151
}
5252

0 commit comments

Comments
 (0)