66
77package scala .tools .nsc .transform .patmat
88
9+ import scala .annotation .tailrec
10+ import scala .collection .immutable .{IndexedSeq , Iterable }
911import scala .language .postfixOps
1012import scala .collection .mutable
1113import scala .reflect .internal .util .Statistics
@@ -514,8 +516,16 @@ trait MatchAnalysis extends MatchApproximation {
514516
515517 // find the models (under which the match fails)
516518 val matchFailModels = findAllModelsFor(propToSolvable(matchFails))
519+
517520 val scrutVar = Var (prevBinderTree)
518- val counterExamples = matchFailModels.flatMap(modelToCounterExample(scrutVar))
521+ val counterExamples = {
522+ matchFailModels.flatMap {
523+ model =>
524+ val varAssignments = expandModel(model)
525+ varAssignments.flatMap(modelToCounterExample(scrutVar) _)
526+ }
527+ }
528+
519529 // sorting before pruning is important here in order to
520530 // keep neg/t7020.scala stable
521531 // since e.g. List(_, _) would cover List(1, _)
@@ -587,6 +597,8 @@ trait MatchAnalysis extends MatchApproximation {
587597 case object WildcardExample extends CounterExample { override def toString = " _" }
588598 case object NoExample extends CounterExample { override def toString = " ??" }
589599
600+ // returns a mapping from variable to
601+ // equal and notEqual symbols
590602 def modelToVarAssignment (model : Model ): Map [Var , (Seq [Const ], Seq [Const ])] =
591603 model.toSeq.groupBy{f => f match {case (sym, value) => sym.variable} }.mapValues{ xs =>
592604 val (trues, falses) = xs.partition(_._2)
@@ -600,20 +612,110 @@ trait MatchAnalysis extends MatchApproximation {
600612 v + " (=" + v.path + " : " + v.staticTpCheckable + " ) " + assignment
601613 }.mkString(" \n " )
602614
603- // return constructor call when the model is a true counter example
604- // (the variables don't take into account type information derived from other variables,
605- // so, naively, you might try to construct a counter example like _ :: Nil(_ :: _, _ :: _),
606- // since we didn't realize the tail of the outer cons was a Nil)
607- def modelToCounterExample (scrutVar : Var )(model : Model ): Option [CounterExample ] = {
615+ /**
616+ * The models we get from the DPLL solver need to be mapped back to counter examples.
617+ * However there's no precalculated mapping model -> counter example. Even worse,
618+ * not every valid model corresponds to a valid counter example.
619+ * The reason is that restricting the valid models further would for example require
620+ * a quadratic number of additional clauses. So to keep the optimistic case fast
621+ * (i.e., all cases are covered in a pattern match), the infeasible counter examples
622+ * are filtered later.
623+ *
624+ * The DPLL procedure keeps the literals that do not contribute to the solution
625+ * unassigned, e.g., for `(a \/ b)`
626+ * only {a = true} or {b = true} is required and the other variable can have any value.
627+ *
628+ * This function does a smart expansion of the model and avoids models that
629+ * have conflicting mappings.
630+ *
631+ * For example for in case of the given set of symbols (taken from `t7020.scala`):
632+ * "V2=2#16"
633+ * "V2=6#19"
634+ * "V2=5#18"
635+ * "V2=4#17"
636+ * "V2=7#20"
637+ *
638+ * One possibility would be to group the symbols by domain but
639+ * this would only work for equality tests and would not be compatible
640+ * with type tests.
641+ * Another observation leads to a much simpler algorithm:
642+ * Only one of these symbols can be set to true,
643+ * since `V2` can at most be equal to one of {2,6,5,4,7}.
644+ */
645+ def expandModel (solution : Solution ): List [Map [Var , (Seq [Const ], Seq [Const ])]] = {
646+
647+ val model = solution.model
648+
608649 // x1 = ...
609650 // x1.hd = ...
610651 // x1.tl = ...
611652 // x1.hd.hd = ...
612653 // ...
613654 val varAssignment = modelToVarAssignment(model)
655+ debug.patmat(" var assignment for model " + model + " :\n " + varAssignmentString(varAssignment))
656+
657+ // group symbols that assign values to the same variables (i.e., symbols are mutually exclusive)
658+ // (thus the groups are sets of disjoint assignments to variables)
659+ val groupedByVar : Map [Var , List [Sym ]] = solution.unassigned.groupBy(_.variable)
660+
661+ val expanded = for {
662+ (variable, syms) <- groupedByVar.toList
663+ } yield {
664+
665+ val (equal, notEqual) = varAssignment.getOrElse(variable, Nil -> Nil )
666+
667+ def addVarAssignment (equalTo : List [Const ], notEqualTo : List [Const ]) = {
668+ Map (variable -> (equal ++ equalTo, notEqual ++ notEqualTo))
669+ }
670+
671+ // this assignment is needed in case that
672+ // there exists already an assign
673+ val allNotEqual = addVarAssignment(Nil , syms.map(_.const))
614674
615- debug.patmat(" var assignment for model " + model + " :\n " + varAssignmentString(varAssignment))
675+ // this assignment is conflicting on purpose:
676+ // a list counter example could contain wildcards: e.g. `List(_,_)`
677+ val allEqual = addVarAssignment(syms.map(_.const), Nil )
616678
679+ if (equal.isEmpty) {
680+ val oneHot = for {
681+ s <- syms
682+ } yield {
683+ addVarAssignment(List (s.const), syms.filterNot(_ == s).map(_.const))
684+ }
685+ allEqual :: allNotEqual :: oneHot
686+ } else {
687+ allEqual :: allNotEqual :: Nil
688+ }
689+ }
690+
691+ if (expanded.isEmpty) {
692+ List (varAssignment)
693+ } else {
694+ // we need the cartesian product here,
695+ // since we want to report all missing cases
696+ // (i.e., combinations)
697+ val cartesianProd = expanded.reduceLeft((xs, ys) =>
698+ for {map1 <- xs
699+ map2 <- ys} yield {
700+ map1 ++ map2
701+ })
702+
703+ // add expanded variables
704+ // note that we can just use `++`
705+ // since the Maps have disjoint keySets
706+ for {
707+ m <- cartesianProd
708+ } yield {
709+ varAssignment ++ m
710+ }
711+ }
712+ }
713+
714+ // return constructor call when the model is a true counter example
715+ // (the variables don't take into account type information derived from other variables,
716+ // so, naively, you might try to construct a counter example like _ :: Nil(_ :: _, _ :: _),
717+ // since we didn't realize the tail of the outer cons was a Nil)
718+ def modelToCounterExample (scrutVar : Var )(varAssignment : Map [Var , (Seq [Const ], Seq [Const ])]): Option [CounterExample ] = {
617719 // chop a path into a list of symbols
618720 def chop (path : Tree ): List [Symbol ] = path match {
619721 case Ident (_) => List (path.symbol)
@@ -742,7 +844,7 @@ trait MatchAnalysis extends MatchApproximation {
742844 // then we can safely ignore these counter examples since we will eventually encounter
743845 // both counter examples separately
744846 case _ if inSameDomain => None
745-
847+
746848 // not a valid counter-example, possibly since we have a definite type but there was a field mismatch
747849 // TODO: improve reasoning -- in the mean time, a false negative is better than an annoying false positive
748850 case _ => Some (NoExample )
0 commit comments