diff options
Diffstat (limited to 'src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala')
-rw-r--r-- | src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala | 679 |
1 files changed, 410 insertions, 269 deletions
diff --git a/src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala b/src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala index c466206192..53c2d16928 100644 --- a/src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala +++ b/src/compiler/scala/tools/nsc/typechecker/PatternMatching.scala @@ -47,7 +47,9 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL val phaseName: String = "patmat" - def patmatDebug(msg: String) = println(msg) + // TODO: the inliner fails to inline the closures to patmatDebug + // private val printPatmat = settings.Ypatmatdebug.value + // @inline final def patmatDebug(s: => String) = if (printPatmat) println(s) def newTransformer(unit: CompilationUnit): Transformer = if (opt.virtPatmat) new MatchTransformer(unit) @@ -143,6 +145,15 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL val typer: Typer val matchOwner = typer.context.owner + def reportUnreachable(pos: Position) = typer.context.unit.warning(pos, "unreachable code") + def reportMissingCases(pos: Position, counterExamples: List[String]) = { + val ceString = + if (counterExamples.tail.isEmpty) "input: " + counterExamples.head + else "inputs: " + counterExamples.mkString(", ") + + typer.context.unit.warning(pos, "match may not be exhaustive.\nIt would fail on the following "+ ceString) + } + def inMatchMonad(tp: Type): Type def pureType(tp: Type): Type final def matchMonadResult(tp: Type): Type = @@ -175,7 +186,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // (that would require more sophistication when generating trees, // and the only place that emits Matches after typers is for exception handling anyway) if(phase.id >= currentRun.uncurryPhase.id) debugwarn("running translateMatch at "+ phase +" on "+ selector +" match "+ cases) - // patmatDebug("translating "+ cases.mkString("{", "\n", "}")) + // patmatDebug ("translating "+ cases.mkString("{", "\n", "}")) def repeatedToSeq(tp: Type): Type = (tp baseType RepeatedParamClass) match { case TypeRef(_, RepeatedParamClass, arg :: Nil) => seqType(arg) @@ -300,13 +311,17 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL if (!extractor.isTyped) ErrorUtils.issueNormalTypeError(patTree, "Could not typecheck extractor call: "+ extractor)(context) // if (extractor.resultInMonad == ErrorType) throw new TypeError(pos, "Unsupported extractor type: "+ extractor.tpe) + // patmatDebug ("translateExtractorPattern checking parameter type: "+ (patBinder, patBinder.info.widen, extractor.paramType, patBinder.info.widen <:< extractor.paramType)) + // must use type `tp`, which is provided by extractor's result, not the type expected by binder, // as b.info may be based on a Typed type ascription, which has not been taken into account yet by the translation // (it will later result in a type test when `tp` is not a subtype of `b.info`) // TODO: can we simplify this, together with the Bound case? - (extractor.subPatBinders, extractor.subPatTypes).zipped foreach { case (b, tp) => b setInfo tp } // patmatDebug("changing "+ b +" : "+ b.info +" -> "+ tp); + (extractor.subPatBinders, extractor.subPatTypes).zipped foreach { case (b, tp) => + // patmatDebug ("changing "+ b +" : "+ b.info +" -> "+ tp) + b setInfo tp + } - // patmatDebug("translateExtractorPattern checking parameter type: "+ (patBinder, patBinder.info.widen, extractor.paramType, patBinder.info.widen <:< extractor.paramType)) // example check: List[Int] <:< ::[Int] // TODO: extractor.paramType may contain unbound type params (run/t2800, run/t3530) val (typeTestTreeMaker, patBinderOrCasted) = @@ -410,7 +425,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL */ case Bind(n, p) => // this happens in certain ill-formed programs, there'll be an error later - // patmatDebug("WARNING: Bind tree with unbound symbol "+ patTree) + // patmatDebug ("WARNING: Bind tree with unbound symbol "+ patTree) noFurtherSubPats() // there's no symbol -- something's wrong... don't fail here though (or should we?) // case Star(_) | ArrayValue | This => error("stone age pattern relics encountered!") @@ -792,7 +807,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def optimizeCases(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type, unchecked: Boolean): (List[List[TreeMaker]], List[Tree]) = (cases, Nil) - def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Tree => Tree]): Option[Tree] = + def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Tree => Tree], unchecked: Boolean): Option[Tree] = None // for catch (no need to customize match failure) @@ -813,7 +828,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL private[TreeMakers] def incorporateOuterSubstitution(outerSubst: Substitution): Unit = { if (currSub ne null) { - // patmatDebug("BUG: incorporateOuterSubstitution called more than once for "+ (this, currSub, outerSubst)) + // patmatDebug ("BUG: incorporateOuterSubstitution called more than once for "+ (this, currSub, outerSubst)) Thread.dumpStack() } else currSub = outerSubst >> substitution @@ -979,7 +994,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL **/ case class TypeTestTreeMaker(prevBinder: Symbol, testedBinder: Symbol, expectedTp: Type, nextBinderTp: Type)(override val pos: Position, extractorArgTypeTest: Boolean = false) extends CondTreeMaker { import TypeTestTreeMaker._ - // patmatDebug("TTTM"+(prevBinder, extractorArgTypeTest, testedBinder, expectedTp, nextBinderTp)) + // patmatDebug ("TTTM"+(prevBinder, extractorArgTypeTest, testedBinder, expectedTp, nextBinderTp)) lazy val outerTestNeeded = ( !((expectedTp.prefix eq NoPrefix) || expectedTp.prefix.typeSymbol.isPackageClass) @@ -1103,11 +1118,11 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL combineCasesNoSubstOnly(scrut, scrutSym, casesNoSubstOnly, pt, owner, matchFailGenOverride) } + // pt is the fully defined type of the cases (either pt or the lub of the types of the cases) def combineCasesNoSubstOnly(scrut: Tree, scrutSym: Symbol, casesNoSubstOnly: List[List[TreeMaker]], pt: Type, owner: Symbol, matchFailGenOverride: Option[Tree => Tree]): Tree = fixerUpper(owner, scrut.pos){ - val ptDefined = if (isFullyDefined(pt)) pt else NoType def matchFailGen = (matchFailGenOverride orElse Some(CODE.MATCHERROR(_: Tree))) - // patmatDebug("combining cases: "+ (casesNoSubstOnly.map(_.mkString(" >> ")).mkString("{", "\n", "}"))) + // patmatDebug ("combining cases: "+ (casesNoSubstOnly.map(_.mkString(" >> ")).mkString("{", "\n", "}"))) val (unchecked, requireSwitch) = if (settings.XnoPatmatAnalysis.value) (true, false) @@ -1120,7 +1135,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL (false, false) } - emitSwitch(scrut, scrutSym, casesNoSubstOnly, pt, matchFailGenOverride).getOrElse{ + emitSwitch(scrut, scrutSym, casesNoSubstOnly, pt, matchFailGenOverride, unchecked).getOrElse{ if (requireSwitch) typer.context.unit.warning(scrut.pos, "could not emit switch for @switch annotated match") if (casesNoSubstOnly nonEmpty) { @@ -1161,12 +1176,12 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL t match { case Function(_, _) if t.symbol == NoSymbol => t.symbol = currentOwner.newAnonymousFunctionValue(t.pos) - // patmatDebug("new symbol for "+ (t, t.symbol.ownerChain)) + // patmatDebug ("new symbol for "+ (t, t.symbol.ownerChain)) case Function(_, _) if (t.symbol.owner == NoSymbol) || (t.symbol.owner == origOwner) => - // patmatDebug("fundef: "+ (t, t.symbol.ownerChain, currentOwner.ownerChain)) + // patmatDebug ("fundef: "+ (t, t.symbol.ownerChain, currentOwner.ownerChain)) t.symbol.owner = currentOwner case d : DefTree if (d.symbol != NoSymbol) && ((d.symbol.owner == NoSymbol) || (d.symbol.owner == origOwner)) => // don't indiscriminately change existing owners! (see e.g., pos/t3440, pos/t3534, pos/unapplyContexts2) - // patmatDebug("def: "+ (d, d.symbol.ownerChain, currentOwner.ownerChain)) + // patmatDebug ("def: "+ (d, d.symbol.ownerChain, currentOwner.ownerChain)) if(d.symbol.isLazy) { // for lazy val's accessor -- is there no tree?? assert(d.symbol.lazyAccessor != NoSymbol && d.symbol.lazyAccessor.owner == d.symbol.owner, d.symbol.lazyAccessor) d.symbol.lazyAccessor.owner = currentOwner @@ -1176,7 +1191,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL d.symbol.owner = currentOwner // case _ if (t.symbol != NoSymbol) && (t.symbol ne null) => - // patmatDebug("untouched "+ (t, t.getClass, t.symbol.ownerChain, currentOwner.ownerChain)) + // patmatDebug ("untouched "+ (t, t.getClass, t.symbol.ownerChain, currentOwner.ownerChain)) case _ => } super.traverse(t) @@ -1208,6 +1223,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // local / context-free def _asInstanceOf(b: Symbol, tp: Type): Tree + def _asInstanceOf(t: Tree, tp: Type, force: Boolean = false): Tree def _equals(checker: Tree, binder: Symbol): Tree def _isInstanceOf(b: Symbol, tp: Type): Tree def and(a: Tree, b: Tree): Tree @@ -1327,10 +1343,6 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL var currId = 0 } case class Test(cond: Cond, treeMaker: TreeMaker) { - // def <:<(other: Test) = cond <:< other.cond - // def andThen_: (prev: List[Test]): List[Test] = - // prev.filterNot(this <:< _) :+ this - // private val reusedBy = new collection.mutable.HashSet[Test] var reuses: Option[Test] = None def registerReuseBy(later: Test): Unit = { @@ -1344,38 +1356,20 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL "T"+ id + "C("+ cond +")" //+ (reuses map ("== T"+_.id) getOrElse (if(reusedBy.isEmpty) treeMaker else reusedBy mkString (treeMaker+ " -->(", ", ",")"))) } + // TODO: remove Cond, replace by Prop from Logic object Cond { - // def refines(self: Cond, other: Cond): Boolean = (self, other) match { - // case (Bottom, _) => true - // case (Havoc , _) => true - // case (_ , Top) => true - // case (_ , _) => false - // } var currId = 0 } abstract class Cond { - // def testedPath: Tree - // def <:<(other: Cond) = Cond.refines(this, other) - val id = { Cond.currId += 1; Cond.currId} } - // does not contribute any knowledge - case object Top extends Cond {override def toString = "T"} - - - // takes away knowledge. e.g., a user-defined guard - case object Havoc extends Cond {override def toString = "_|_"} - - // we know everything! everything! - // this either means the case is unreachable, - // or that it is statically known to be picked -- at this point in the decision tree --> no point in emitting further alternatives - // case object Bottom extends Cond - + case object TrueCond extends Cond {override def toString = "T"} + case object FalseCond extends Cond {override def toString = "F"} case class AndCond(a: Cond, b: Cond) extends Cond {override def toString = a +"/\\"+ b} - case class OrCond(a: Cond, b: Cond) extends Cond {override def toString = "("+a+") \\/ ("+ b +")"} + case class OrCond(a: Cond, b: Cond) extends Cond {override def toString = "("+a+") \\/ ("+ b +")"} object EqualityCond { private val uniques = new collection.mutable.HashMap[(Tree, Tree), EqualityCond] @@ -1383,12 +1377,6 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def unapply(c: EqualityCond) = Some(c.testedPath, c.rhs) } class EqualityCond(val testedPath: Tree, val rhs: Tree) extends Cond { - // def negation = TopCond // inequality doesn't teach us anything - // do simplification when we know enough about the tree statically: - // - collapse equal trees - // - accumulate tests when (in)equality not known statically - // - become bottom when we statically know this can never match - override def toString = testedPath +" == "+ rhs +"#"+ id } @@ -1407,11 +1395,6 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def unapply(c: TypeCond) = Some(c.testedPath, c.pt) } class TypeCond(val testedPath: Tree, val pt: Type) extends Cond { - // def negation = TopCond // inequality doesn't teach us anything - // do simplification when we know enough about the tree statically: - // - collapse equal trees - // - accumulate tests when (in)equality not known statically - // - become bottom when we statically know this can never match override def toString = testedPath +" : "+ pt +"#"+ id } @@ -1434,9 +1417,27 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL case _ => false }) + object IrrefutableExtractorTreeMaker { + // will an extractor with unapply method of methodtype `tp` always succeed? + // note: this assumes the other side-conditions implied by the extractor are met + // (argument of the right type, length check succeeds for unapplySeq,...) + def irrefutableExtractorType(tp: Type): Boolean = tp.resultType.dealias match { + case TypeRef(_, SomeClass, _) => true + // probably not useful since this type won't be inferred nor can it be written down (yet) + case ConstantType(Constant(true)) => true + case _ => false + } + + def unapply(xtm: ExtractorTreeMaker): Option[(Tree, Symbol, Substitution)] = xtm match { + case ExtractorTreeMaker(extractor, None, nextBinder, subst) if irrefutableExtractorType(extractor.tpe) => + Some(extractor, nextBinder, subst) + case _ => + None + } + } // returns (tree, tests), where `tree` will be used to refer to `root` in `tests` - abstract class TreeMakersToConds(val root: Symbol) { + class TreeMakersToConds(val root: Symbol) { def discard() = { pointsToBound.clear() trees.clear() @@ -1471,20 +1472,22 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // reverse substitution that would otherwise replace a variable we already encountered by a new variable // NOTE: this forgets the more precise type we have for these later variables, but that's probably okay normalize >>= Substitution(boundTo map (_.symbol), boundFrom map (CODE.REF(_))) - // patmatDebug("normalize: "+ normalize) + // patmatDebug ("normalize subst: "+ normalize) val okSubst = Substitution(unboundFrom, unboundTo map (normalize(_))) // it's important substitution does not duplicate trees here -- it helps to keep hash consing simple, anyway pointsToBound ++= ((okSubst.from, okSubst.to).zipped filter { (f, t) => pointsToBound exists (sym => t.exists(_.symbol == sym)) })._1 - // patmatDebug("pointsToBound: "+ pointsToBound) + // patmatDebug ("pointsToBound: "+ pointsToBound) accumSubst >>= okSubst - // patmatDebug("accumSubst: "+ accumSubst) + // patmatDebug ("accumSubst: "+ accumSubst) } // hashconsing trees (modulo value-equality) def unique(t: Tree, tpOverride: Type = NoType): Tree = trees find (a => a.correspondsStructure(t)(sameValue)) match { - case Some(orig) => orig // patmatDebug("unique: "+ (t eq orig, orig)); + case Some(orig) => + // patmatDebug("unique: "+ (t eq orig, orig)) + orig case _ => trees += t if (tpOverride != NoType) t setType tpOverride @@ -1504,27 +1507,20 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL final def binderToUniqueTree(b: Symbol) = unique(accumSubst(normalize(CODE.REF(b))), b.tpe) - @inline def /\(conds: Iterable[Cond]) = if (conds.isEmpty) Top else conds.reduceLeft(AndCond(_, _)) - @inline def \/(conds: Iterable[Cond]) = if (conds.isEmpty) Havoc else conds.reduceLeft(OrCond(_, _)) + @inline def /\(conds: Iterable[Cond]) = if (conds.isEmpty) TrueCond else conds.reduceLeft(AndCond(_, _)) + @inline def \/(conds: Iterable[Cond]) = if (conds.isEmpty) FalseCond else conds.reduceLeft(OrCond(_, _)) // note that the sequencing of operations is important: must visit in same order as match execution // binderToUniqueTree uses the type of the first symbol that was encountered as the type for all future binders - final protected def treeMakerToCond(tm: TreeMaker, condMaker: CondMaker): Cond = { - updateSubstitution(tm.substitution) - condMaker(tm)(treeMakerToCond(_, condMaker)) - } + final def treeMakerToCond(tm: TreeMaker, handleUnknown: TreeMaker => Cond, updateSubst: Boolean, rewriteNil: Boolean = false): Cond = { + if (updateSubst) updateSubstitution(tm.substitution) - final protected def treeMakerToCondNoSubst(tm: TreeMaker, condMaker: CondMaker): Cond = - condMaker(tm)(treeMakerToCondNoSubst(_, condMaker)) - - type CondMaker = TreeMaker => (TreeMaker => Cond) => Cond - final def makeCond(tm: TreeMaker)(recurse: TreeMaker => Cond): Cond = { tm match { case ttm@TypeTestTreeMaker(prevBinder, testedBinder, pt, _) => object condStrategy extends TypeTestTreeMaker.TypeTestCondStrategy { type Result = Cond def and(a: Result, b: Result) = AndCond(a, b) - def outerTest(testedBinder: Symbol, expectedTp: Type) = Top // TODO OuterEqCond(testedBinder, expectedType) + def outerTest(testedBinder: Symbol, expectedTp: Type) = TrueCond // TODO OuterEqCond(testedBinder, expectedType) def typeTest(b: Symbol, pt: Type) = { // a type test implies the tested path is non-null (null.isInstanceOf[T] is false for all T) val p = binderToUniqueTree(b); AndCond(NonNullCond(p), TypeCond(p, uniqueTp(pt))) } @@ -1534,34 +1530,56 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } ttm.renderCondition(condStrategy) case EqualityTestTreeMaker(prevBinder, patTree, _) => EqualityCond(binderToUniqueTree(prevBinder), unique(patTree)) - case AlternativesTreeMaker(_, altss, _) => \/(altss map (alts => /\(alts map recurse))) + case AlternativesTreeMaker(_, altss, _) => \/(altss map (alts => /\(alts map (treeMakerToCond(_, handleUnknown, updateSubst))))) case ProductExtractorTreeMaker(testedBinder, None, subst) => NonNullCond(binderToUniqueTree(testedBinder)) - case ExtractorTreeMaker(_, _, _, _) - | GuardTreeMaker(_) - | ProductExtractorTreeMaker(_, Some(_), _) - | BodyTreeMaker(_, _) => Havoc - case SubstOnlyTreeMaker(_, _) => Top + case IrrefutableExtractorTreeMaker(_, _, _) => + // the extra condition is None, the extractor's result indicates it always succeeds, + // and the potential type-test for the argument is represented by a separate TypeTestTreeMaker + TrueCond + case GuardTreeMaker(guard) => + guard.tpe match { + case ConstantType(Constant(true)) => TrueCond + case ConstantType(Constant(false)) => FalseCond + case _ => handleUnknown(tm) + } + case p @ ExtractorTreeMaker(extractor, Some(lenCheck), testedBinder, _) => + p.checkedLength match { + // special-case: interpret pattern `List()` as `Nil` + // TODO: make it more general List(1, 2) => 1 :: 2 :: Nil -- not sure this is a good idea... + case Some(0) if rewriteNil && testedBinder.tpe.typeSymbol == ListClass => // extractor.symbol.owner == SeqFactory + EqualityCond(binderToUniqueTree(p.prevBinder), unique(Ident(NilModule) setType NilModule.tpe)) + case _ => handleUnknown(tm) + } + case SubstOnlyTreeMaker(_, _) => TrueCond + case ProductExtractorTreeMaker(_, Some(_), _) | + ExtractorTreeMaker(_, _, _, _) | BodyTreeMaker(_, _) => handleUnknown(tm) } } - final def approximateMatch(cases: List[List[TreeMaker]], condMaker: CondMaker = makeCond): List[List[Test]] = cases.map { _ map (tm => Test(treeMakerToCond(tm, condMaker), tm)) } + val constFalse = (_: TreeMaker) => FalseCond + val constTrue = (_: TreeMaker) => TrueCond + + final def approximateMatch(cases: List[List[TreeMaker]], handleUnknown: TreeMaker => Cond = constFalse, rewriteNil: Boolean = false): List[List[Test]] = + cases.map { _ map (tm => Test(treeMakerToCond(tm, handleUnknown, updateSubst = true, rewriteNil), tm)) } - final def approximateMatchAgain(cases: List[List[TreeMaker]], condMaker: CondMaker = makeCond): List[List[Test]] = cases.map { _ map (tm => Test(treeMakerToCondNoSubst(tm, condMaker), tm)) } + final def approximateMatchAgain(cases: List[List[TreeMaker]], handleUnknown: TreeMaker => Cond = constFalse, rewriteNil: Boolean = false): List[List[Test]] = + cases.map { _ map (tm => Test(treeMakerToCond(tm, handleUnknown, updateSubst = false, rewriteNil), tm)) } } + def approximateMatch(root: Symbol, cases: List[List[TreeMaker]]): List[List[Test]] = { object approximator extends TreeMakersToConds(root) approximator.approximateMatch(cases) } def showTreeMakers(cases: List[List[TreeMaker]]) = { - patmatDebug("treeMakers:") - patmatDebug(alignAcrossRows(cases, ">>")) + // patmatDebug ("treeMakers:") + // patmatDebug (alignAcrossRows(cases, ">>")) } def showTests(testss: List[List[Test]]) = { - patmatDebug("tests: ") - patmatDebug(alignAcrossRows(testss, "&")) + // patmatDebug ("tests: ") + // patmatDebug (alignAcrossRows(testss, "&")) } } @@ -1597,24 +1615,38 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL case class Eq(p: Var, q: Const) extends Prop type Const <: AbsConst - trait AbsConst { def implies(other: Const): Boolean def excludes(other: Const): Boolean } + type TypeConst <: Const + def TypeConst: TypeConstExtractor + trait TypeConstExtractor { + def apply(tp: Type): Const + } + + def NullConst: Const + type Var <: AbsVar trait AbsVar { // indicate we may later require a prop for V = C def registerEquality(c: Const): Unit - // indicates null is part of the domain - def considerNull: Unit + // call this to indicate null is part of the domain + def registerNull: Unit + + // can this variable be null? + def mayBeNull: Boolean - // compute the domain and return it (call considerNull first!) + // compute the domain and return it (call registerNull first!) def domainSyms: Option[Set[Sym]] + // the symbol for this variable being equal to its statically known type + // (only available if registerEquality has been called for that type before) + def symForStaticTp: Option[Sym] + // for this var, call it V, turn V = C into the equivalent proposition in boolean logic // registerEquality(c) must have been called prior to this call // in fact, all equalities relevant to this variable must have been registered @@ -1692,7 +1724,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // // TODO: for V1 representing x1 and V2 standing for x1.head, encode that // V1 = Nil implies -(V2 = Ci) for all Ci in V2's domain (i.e., it is unassignable) - def removeVarEq(props: List[Prop], considerNull: Boolean = false): (Prop, List[Prop]) = { + def removeVarEq(props: List[Prop], modelNull: Boolean = false): (Prop, List[Prop]) = { val start = Statistics.startTimer(patmatAnaVarEq) val vars = new collection.mutable.HashSet[Var] @@ -1714,7 +1746,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } props foreach gatherEqualities.apply - if (considerNull) vars foreach (_.considerNull) + if (modelNull) vars foreach (_.registerNull) val pure = props map rewriteEqualsToProp.apply @@ -1730,7 +1762,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL override def hashCode = a.hashCode ^ b.hashCode } - // patmatDebug("vars: "+ vars) + // patmatDebug ("removeVarEq vars: "+ vars) vars.foreach { v => val excludedPair = new collection.mutable.HashSet[ExcludedPair] @@ -1741,15 +1773,24 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // coverage is formulated as: A \/ B \/ C and the implications are v.domainSyms foreach { dsyms => addAxiom(\/(dsyms)) } + // when this variable cannot be null the equality corresponding to the type test `(x: T)`, where T is x's static type, + // is always true; when the variable may be null we use the implication `(x != null) => (x: T)` for the axiom + v.symForStaticTp foreach { symForStaticTp => + if (v.mayBeNull) addAxiom(Or(v.propForEqualsTo(NullConst), symForStaticTp)) + else addAxiom(symForStaticTp) + } + val syms = v.equalitySyms - // patmatDebug("eqSyms "+(v, syms)) + // patmatDebug ("eqSyms "+(v, syms)) syms foreach { sym => // if we've already excluded the pair at some point (-A \/ -B), then don't exclude the symmetric one (-B \/ -A) // (nor the positive implications -B \/ A, or -A \/ B, which would entail the equality axioms falsifying the whole formula) val todo = syms filterNot (b => (b.const == sym.const) || excludedPair(ExcludedPair(b.const, sym.const))) val (excluded, notExcluded) = todo partition (b => sym.const.excludes(b.const)) val implied = notExcluded filter (b => sym.const.implies(b.const)) - // patmatDebug("implications: "+ (sym.const, excluded, implied, syms)) + // patmatDebug ("eq axioms for: "+ sym.const) + // patmatDebug ("excluded: "+ excluded) + // patmatDebug ("implied: "+ implied) // when this symbol is true, what must hold... implied foreach (impliedSym => addAxiom(Or(Not(sym), impliedSym))) @@ -1762,8 +1803,8 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } } - // patmatDebug("eqAxioms:\n"+ cnfString(eqFreePropToSolvable(eqAxioms))) - // patmatDebug("pure:\n"+ cnfString(eqFreePropToSolvable(pure))) + // patmatDebug ("eqAxioms:\n"+ cnfString(eqFreePropToSolvable(eqAxioms))) + // patmatDebug ("pure:"+ pure.map(p => cnfString(eqFreePropToSolvable(p))).mkString("\n")) Statistics.stopTimer(patmatAnaVarEq, start) @@ -1778,7 +1819,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // may throw an CNFBudgetExceeded def propToSolvable(p: Prop) = { - val (eqAxioms, pure :: Nil) = removeVarEq(List(p), considerNull = false) + val (eqAxioms, pure :: Nil) = removeVarEq(List(p), modelNull = false) eqFreePropToSolvable(And(eqAxioms, pure)) } @@ -1905,12 +1946,12 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def findAllModels(f: Formula, models: List[Model], recursionDepthAllowed: Int = 10): List[Model]= if (recursionDepthAllowed == 0) models else { - // patmatDebug("solving\n"+ cnfString(f)) + // patmatDebug ("find all models for\n"+ cnfString(f)) val model = findModelFor(f) // if we found a solution, conjunct the formula with the model's negation and recurse if (model ne NoModel) { val unassigned = (vars -- model.keySet).toList - // patmatDebug("unassigned "+ unassigned +" in "+ model) + // patmatDebug ("unassigned "+ unassigned +" in "+ model) def force(lit: Lit) = { val model = withLit(findModelFor(dropUnit(f, lit)), lit) if (model ne NoModel) List(model) @@ -1919,7 +1960,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL val forced = unassigned flatMap { s => force(Lit(s, true)) ++ force(Lit(s, false)) } - // patmatDebug("forced "+ forced) + // patmatDebug ("forced "+ forced) val negated = negateModel(model) findAllModels(f :+ negated, model :: (forced ++ models), recursionDepthAllowed - 1) } @@ -1942,7 +1983,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def findModelFor(f: Formula): Model = { @inline def orElse(a: Model, b: => Model) = if (a ne NoModel) a else b - // patmatDebug("dpll\n"+ cnfString(f)) + // patmatDebug ("DPLL\n"+ cnfString(f)) val start = Statistics.startTimer(patmatAnaDPLL) @@ -2004,7 +2045,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL private val uniques = new collection.mutable.HashMap[Tree, Var] def apply(x: Tree): Var = uniques getOrElseUpdate(x, new Var(x, x.tpe)) } - class Var(val path: Tree, fullTp: Type) extends AbsVar { + class Var(val path: Tree, staticTp: Type) extends AbsVar { private[this] val id: Int = Var.nextId // private[this] var canModify: Option[Array[StackTraceElement]] = None @@ -2016,10 +2057,11 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL private[this] val symForEqualsTo = new collection.mutable.HashMap[Const, Sym] // when looking at the domain, we only care about types we can check at run time - val domainTp: Type = checkableType(fullTp) + val staticTpCheckable: Type = checkableType(staticTp) - private[this] var _considerNull = false - def considerNull: Unit = { ensureCanModify; if (NullTp <:< domainTp) _considerNull = true } + private[this] var _mayBeNull = false + def registerNull: Unit = { ensureCanModify; if (NullTp <:< staticTpCheckable) _mayBeNull = true } + def mayBeNull: Boolean = _mayBeNull // case None => domain is unknown, // case Some(List(tps: _*)) => domain is exactly tps @@ -2027,7 +2069,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // once we go to run-time checks (on Const's), convert them to checkable types // TODO: there seems to be bug for singleton domains (variable does not show up in model) lazy val domain: Option[Set[Const]] = { - val subConsts = enumerateSubtypes(fullTp).map{ tps => + val subConsts = enumerateSubtypes(staticTp).map{ tps => tps.toSet[Type].map{ tp => val domainC = TypeConst(tp) registerEquality(domainC) @@ -2036,18 +2078,19 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } val allConsts = - if (! _considerNull) subConsts - else { + if (mayBeNull) { registerEquality(NullConst) subConsts map (_ + NullConst) - } + } else + subConsts observed; allConsts } - // accessing after calling considerNull will result in inconsistencies + // accessing after calling registerNull will result in inconsistencies lazy val domainSyms: Option[Set[Sym]] = domain map { _ map symForEqualsTo } + lazy val symForStaticTp: Option[Sym] = symForEqualsTo.get(TypeConst(staticTpCheckable)) // populate equalitySyms // don't care about the result, but want only one fresh symbol per distinct constant c @@ -2061,8 +2104,8 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def propForEqualsTo(c: Const): Prop = {observed; symForEqualsTo.getOrElse(c, False)} - // don't call until all equalities have been registered and considerNull has been called (if needed) - def describe = toString + ": " + fullTp + domain.map(_.mkString(" ::= ", " | ", "// "+ symForEqualsTo.keys)).getOrElse(symForEqualsTo.keys.mkString(" ::= ", " | ", " | ...")) + " // = " + path + // don't call until all equalities have been registered and registerNull has been called (if needed) + def describe = toString + ": " + staticTp + domain.map(_.mkString(" ::= ", " | ", "// "+ symForEqualsTo.keys)).getOrElse(symForEqualsTo.keys.mkString(" ::= ", " | ", " | ...")) + " // = " + path override def toString = "V"+ id } @@ -2071,7 +2114,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // a literal constant becomes ConstantType(Constant(v)) when the type allows it (roughly, anyval + string + null) // equality between variables: SingleType(x) (note that pattern variables cannot relate to each other -- it's always patternVar == nonPatternVar) object Const { - def resetUniques() = {_nextTypeId = 0; _nextValueId = 0; uniques.clear() ; trees.clear() } // patmatDebug("RESET") + def resetUniques() = {_nextTypeId = 0; _nextValueId = 0; uniques.clear() ; trees.clear()} private var _nextTypeId = 0 def nextTypeId = {_nextTypeId += 1; _nextTypeId} @@ -2083,9 +2126,12 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL private[SymbolicMatchAnalysis] def unique(tp: Type, mkFresh: => Const): Const = uniques.get(tp).getOrElse( uniques.find {case (oldTp, oldC) => oldTp =:= tp} match { - case Some((_, c)) => c + case Some((_, c)) => + // patmatDebug ("unique const: "+ (tp, c)) + c case _ => val fresh = mkFresh + // patmatDebug ("uniqued const: "+ (tp, fresh)) uniques(tp) = fresh fresh }) @@ -2101,19 +2147,19 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL if (!t.symbol.isStable) t.tpe.narrow else trees find (a => a.correspondsStructure(t)(sameValue)) match { case Some(orig) => - // patmatDebug("unique: "+ (orig, orig.tpe)) + // patmatDebug ("unique tp for tree: "+ (orig, orig.tpe)) orig.tpe case _ => // duplicate, don't mutate old tree (TODO: use a map tree -> type instead?) val treeWithNarrowedType = t.duplicate setType t.tpe.narrow - // patmatDebug("uniqued: "+ (t, t.tpe, treeWithNarrowedType.tpe)) + // patmatDebug ("uniqued: "+ (t, t.tpe, treeWithNarrowedType.tpe)) trees += treeWithNarrowedType treeWithNarrowedType.tpe } } sealed abstract class Const extends AbsConst { - protected def tp: Type + def tp: Type protected def wideTp: Type def isAny = wideTp.typeSymbol == AnyClass @@ -2144,7 +2190,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL case (_: TypeConst, _: TypeConst) => !((tp <:< other.tp) || (other.tp <:< tp)) case _ => false } - // if(r) patmatDebug("excludes : "+(this, other)) + // if(r) patmatDebug("excludes : "+(this, this.tp, other, other.tp, (tp <:< other.tp), (tp <:< other.wideTp), (other.tp <:< tp), (other.tp <:< wideTp))) // else patmatDebug("NOT excludes: "+(this, other)) r } @@ -2169,7 +2215,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL clsTp } - object TypeConst { + object TypeConst extends TypeConstExtractor { def apply(tp: Type) = { if (tp =:= NullTp) NullConst else if (tp.isInstanceOf[SingletonType]) ValueConst.fromType(tp) @@ -2215,7 +2261,6 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // for Selects, which are handled by the next case, the prefix of the select varies independently of the symbol (see pos/virtpatmat_unreach_select.scala) singleType(tp.prefix, p.symbol) case _ => - // patmatDebug("unique type for "+(p, Const.uniqueTpForTree(p))) Const.uniqueTpForTree(p) } @@ -2235,7 +2280,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL lazy val NullTp = ConstantType(Constant(null)) case object NullConst extends Const { - protected def tp = NullTp + def tp = NullTp protected def wideTp = NullTp def isValue = true @@ -2249,8 +2294,8 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def symbolic(t: Cond): Prop = t match { case AndCond(a, b) => And(symbolic(a), symbolic(b)) case OrCond(a, b) => Or(symbolic(a), symbolic(b)) - case Top => True - case Havoc => False + case TrueCond => True + case FalseCond => False case TypeCond(p, pt) => Eq(Var(p), TypeConst(checkableType(pt))) case EqualityCond(p, q) => Eq(Var(p), ValueConst(q)) case NonNullCond(p) => if (!modelNull) True else Not(Eq(Var(p), NullConst)) @@ -2274,46 +2319,21 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // thus, the case is unreachable if there is no model for -(-P /\ C), // or, equivalently, P \/ -C, or C => P def unreachableCase(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type): Option[Int] = { - // customize TreeMakersToConds (which turns a tree of tree makers into a more abstract DAG of tests) - // when approximating the current case (which we hope is reachable), be optimistic about the unknowns - object reachabilityApproximation extends TreeMakersToConds(prevBinder) { - def makeCondOptimistic(tm: TreeMaker)(recurse: TreeMaker => Cond): Cond = tm match { - // for unreachability, let's assume a guard always matches (unless we statically determined otherwise) - // otherwise, a guarded case would be considered unreachable - case GuardTreeMaker(guard) => - guard.tpe match { - case ConstantType(Constant(false)) => Havoc // not the best name; however, symbolically, 'Havoc' becomes 'False' - case _ => Top - } - // similar to a guard, user-defined extractors should not cause us to freak out - // if we're not 100% sure it does not match (i.e., its result type is None or Constant(false) -- TODO), - // let's stay optimistic and assume it does - case ExtractorTreeMaker(_, _, _, _) - | ProductExtractorTreeMaker(_, Some(_), _) => Top - // TODO: consider length-checks - case _ => - makeCond(tm)(recurse) - } - - // be pessimistic when approximating the prefix of the current case - // we hope the prefix fails so that we might get to the current case, which we hope is not dead - def makeCondPessimistic(tm: TreeMaker)(recurse: TreeMaker => Cond): Cond = makeCond(tm)(recurse) - } - val start = Statistics.startTimer(patmatAnaReach) // use the same approximator so we share variables, // but need different conditions depending on whether we're conservatively looking for failure or success - val testCasesOk = reachabilityApproximation.approximateMatch(cases, reachabilityApproximation.makeCondOptimistic) - val testCasesFail = reachabilityApproximation.approximateMatchAgain(cases, reachabilityApproximation.makeCondPessimistic) + val reachabilityApproximation = new TreeMakersToConds(prevBinder) + val testCasesOk = reachabilityApproximation.approximateMatch(cases, reachabilityApproximation.constTrue) + val testCasesFail = reachabilityApproximation.approximateMatchAgain(cases, reachabilityApproximation.constFalse) reachabilityApproximation.discard() prepareNewAnalysis() val propsCasesOk = testCasesOk map (t => symbolicCase(t, modelNull = true)) val propsCasesFail = testCasesFail map (t => Not(symbolicCase(t, modelNull = true))) - val (eqAxiomsFail, symbolicCasesFail) = removeVarEq(propsCasesFail, considerNull = true) - val (eqAxiomsOk, symbolicCasesOk) = removeVarEq(propsCasesOk, considerNull = true) + val (eqAxiomsFail, symbolicCasesFail) = removeVarEq(propsCasesFail, modelNull = true) + val (eqAxiomsOk, symbolicCasesOk) = removeVarEq(propsCasesOk, modelNull = true) try { // most of the time eqAxiomsFail == eqAxiomsOk, but the different approximations might cause different variables to disapper in general @@ -2327,8 +2347,8 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL var reachable = true var caseIndex = 0 - // patmatDebug("reachability, vars:\n"+ ((propsCasesFail flatMap gatherVariables) map (_.describe) mkString ("\n"))) - // patmatDebug("equality axioms:\n"+ cnfString(eqAxiomsCNF)) + // patmatDebug ("reachability, vars:\n"+ ((propsCasesFail flatMap gatherVariables) map (_.describe) mkString ("\n"))) + // patmatDebug ("equality axioms:\n"+ cnfString(eqAxiomsCNF)) // invariant (prefixRest.length == current.length) && (prefix.reverse ++ prefixRest == symbolicCasesFail) // termination: prefixRest.length decreases by 1 @@ -2369,14 +2389,13 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL case UnitClass => Some(List(UnitClass.tpe)) case BooleanClass => - // patmatDebug("enum bool "+ tp) Some(List(ConstantType(Constant(true)), ConstantType(Constant(false)))) // TODO case _ if tp.isTupleType => // recurse into component types case modSym: ModuleClassSymbol => Some(List(tp)) // make sure it's not a primitive, else (5: Byte) match { case 5 => ... } sees no Byte case sym if !sym.isSealed || isPrimitiveValueClass(sym) => - // patmatDebug("enum unsealed "+ (tp, sym, sym.isSealed, isPrimitiveValueClass(sym))) + // patmatDebug ("enum unsealed "+ (tp, sym, sym.isSealed, isPrimitiveValueClass(sym))) None case sym => val subclasses = ( @@ -2384,7 +2403,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // symbols which are both sealed and abstract need not be covered themselves, because // all of their children must be and they cannot otherwise be created. filterNot (x => x.isSealed && x.isAbstractClass && !isPrimitiveValueClass(x))) - // patmatDebug("subclasses "+ (sym, subclasses)) + // patmatDebug ("enum sealed -- subclasses: "+ (sym, subclasses)) val tpApprox = typer.infer.approximateAbstracts(tp) val pre = tpApprox.prefix @@ -2400,7 +2419,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL if (subTpApprox <:< tpApprox) Some(checkableType(subTp)) else None }) - // patmatDebug("enum sealed "+ (tp, tpApprox) + " as "+ validSubTypes) + // patmatDebug ("enum sealed "+ (tp, tpApprox) + " as "+ validSubTypes) Some(validSubTypes) } @@ -2420,7 +2439,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } } val res = toCheckable(tp) - // patmatDebug("checkable "+(tp, res)) + // patmatDebug ("checkable "+(tp, res)) res } @@ -2444,37 +2463,15 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // - there are extractor calls (that we can't secretly/soundly) rewrite val start = Statistics.startTimer(patmatAnaExhaust) var backoff = false - object exhaustivityApproximation extends TreeMakersToConds(prevBinder) { - def makeCondExhaustivity(tm: TreeMaker)(recurse: TreeMaker => Cond): Cond = tm match { - case p @ ExtractorTreeMaker(extractor, Some(lenCheck), testedBinder, _) => - p.checkedLength match { - // pattern: `List()` (interpret as `Nil`) - // TODO: make it more general List(1, 2) => 1 :: 2 :: Nil - case Some(0) if testedBinder.tpe.typeSymbol == ListClass => // extractor.symbol.owner == SeqFactory - EqualityCond(binderToUniqueTree(p.prevBinder), unique(Ident(NilModule) setType NilModule.tpe)) - case _ => - backoff = true - makeCond(tm)(recurse) - } - case ExtractorTreeMaker(_, _, _, _) => -// patmatDebug("backing off due to "+ tm) - backoff = true - makeCond(tm)(recurse) - case GuardTreeMaker(guard) => - guard.tpe match { - case ConstantType(Constant(true)) => Top - case ConstantType(Constant(false)) => Havoc - case _ => -// patmatDebug("can't statically interpret guard: "+(guard, guard.tpe)) - backoff = true - Havoc - } - case _ => - makeCond(tm)(recurse) - } - } - val tests = exhaustivityApproximation.approximateMatch(cases, exhaustivityApproximation.makeCondExhaustivity) + val exhaustivityApproximation = new TreeMakersToConds(prevBinder) + val tests = exhaustivityApproximation.approximateMatch(cases, { + case BodyTreeMaker(_, _) => TrueCond // will be discarded by symbolCase later + case tm => + // patmatDebug("backing off due to "+ tm) + backoff = true + FalseCond + }, rewriteNil = true) if (backoff) Nil else { val prevBinderTree = exhaustivityApproximation.binderToUniqueTree(prevBinder) @@ -2497,15 +2494,14 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // when does the match fail? val matchFails = Not(\/(symbolicCases)) + val vars = gatherVariables(matchFails) // debug output: - // patmatDebug("analysing:") - // showTreeMakers(cases) - // showTests(tests) - // - // val vars = gatherVariables(matchFails) + // patmatDebug ("analysing:") + showTreeMakers(cases) + showTests(tests) + // patmatDebug("\nvars:\n"+ (vars map (_.describe) mkString ("\n"))) - // // patmatDebug("\nmatchFails as CNF:\n"+ cnfString(propToSolvable(matchFails))) try { @@ -2521,7 +2517,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL pruned } catch { case e : CNFBudgetExceeded => - // patmatDebug(util.Position.formatMessage(prevBinder.pos, "Cannot check match for exhaustivity", false)) + // patmatDebug (util.Position.formatMessage(prevBinder.pos, "Cannot check match for exhaustivity", false)) // e.printStackTrace() Nil // CNF budget exceeded } @@ -2542,13 +2538,13 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } case class ValueExample(c: ValueConst) extends CounterExample { override def toString = c.toString } case class TypeExample(c: Const) extends CounterExample { override def toString = "(_ : "+ c +")" } - case class NegativeExample(nonTrivialNonEqualTo: List[Const]) extends CounterExample { + case class NegativeExample(eqTo: Const, nonTrivialNonEqualTo: List[Const]) extends CounterExample { // require(nonTrivialNonEqualTo.nonEmpty, nonTrivialNonEqualTo) override def toString = { val negation = if (nonTrivialNonEqualTo.tail.isEmpty) nonTrivialNonEqualTo.head.toString - else nonTrivialNonEqualTo.map(_.toString).sorted.mkString("in (", ", ", ")") - "<not "+ negation +">" + else nonTrivialNonEqualTo.map(_.toString).sorted.mkString("(", ", ", ")") + "(x: "+ eqTo +" forSome x not in "+ negation +")" } } case class ListExample(ctorArgs: List[CounterExample]) extends CounterExample { @@ -2594,7 +2590,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def varAssignmentString(varAssignment: Map[Var, (Seq[Const], Seq[Const])]) = varAssignment.toSeq.sortBy(_._1.toString).map { case (v, (trues, falses)) => val assignment = "== "+ (trues mkString("(", ", ", ")")) +" != ("+ (falses mkString(", ")) +")" - v +"(="+ v.path +": "+ v.domainTp +") "+ assignment + v +"(="+ v.path +": "+ v.staticTpCheckable +") "+ assignment }.mkString("\n") def modelString(model: Model) = varAssignmentString(modelToVarAssignment(model)) @@ -2611,13 +2607,14 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // ... val varAssignment = modelToVarAssignment(model) - // patmatDebug("var assignment for model "+ model +":\n"+ varAssignmentString(varAssignment)) + // patmatDebug ("var assignment for model "+ model +":\n"+ varAssignmentString(varAssignment)) // chop a path into a list of symbols def chop(path: Tree): List[Symbol] = path match { case Ident(_) => List(path.symbol) case Select(pre, name) => chop(pre) :+ path.symbol - case _ => // patmatDebug("don't know how to chop "+ path) + case _ => + // patmatDebug("don't know how to chop "+ path) Nil } @@ -2658,8 +2655,9 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // node in the tree that describes how to construct a counter-example case class VariableAssignment(variable: Var, equalTo: List[Const], notEqualTo: List[Const], fields: collection.mutable.Map[Symbol, VariableAssignment]) { // need to prune since the model now incorporates all super types of a constant (needed for reachability) - private lazy val prunedEqualTo = equalTo filterNot (subsumed => equalTo.exists(better => (better ne subsumed) && (better implies subsumed))) - private lazy val ctor = (prunedEqualTo match { case List(TypeConst(tp)) => tp case _ => variable.domainTp }).typeSymbol.primaryConstructor + private lazy val uniqueEqualTo = equalTo filterNot (subsumed => equalTo.exists(better => (better ne subsumed) && (better implies subsumed))) + private lazy val prunedEqualTo = uniqueEqualTo filterNot (subsumed => variable.staticTpCheckable <:< subsumed.tp) + private lazy val ctor = (prunedEqualTo match { case List(TypeConst(tp)) => tp case _ => variable.staticTpCheckable }).typeSymbol.primaryConstructor private lazy val ctorParams = if (ctor == NoSymbol || ctor.paramss.isEmpty) Nil else ctor.paramss.head private lazy val cls = if (ctor == NoSymbol) NoSymbol else ctor.owner private lazy val caseFieldAccs = if (cls == NoSymbol) Nil else cls.caseFieldAccessors @@ -2676,7 +2674,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def toCounterExample(beBrief: Boolean = false): CounterExample = if (!allFieldAssignmentsLegal) NoExample else { - // patmatDebug("describing "+ (variable, equalTo, notEqualTo, fields, cls, allFieldAssignmentsLegal)) + // patmatDebug ("describing "+ (variable, equalTo, notEqualTo, fields, cls, allFieldAssignmentsLegal)) val res = prunedEqualTo match { // a definite assignment to a value case List(eq: ValueConst) if fields.isEmpty => ValueExample(eq) @@ -2684,9 +2682,9 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // constructor call // or we did not gather any information about equality but we have information about the fields // --> typical example is when the scrutinee is a tuple and all the cases first unwrap that tuple and only then test something interesting - case _ if cls != NoSymbol && - ( prunedEqualTo.nonEmpty - || (fields.nonEmpty && !isPrimitiveValueClass(cls) && prunedEqualTo.isEmpty && notEqualTo.isEmpty)) => + case _ if cls != NoSymbol && !isPrimitiveValueClass(cls) && + ( uniqueEqualTo.nonEmpty + || (fields.nonEmpty && prunedEqualTo.isEmpty && notEqualTo.isEmpty)) => @inline def args(brevity: Boolean = beBrief) = { // figure out the constructor arguments from the field assignment @@ -2707,13 +2705,17 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // negative information case Nil if nonTrivialNonEqualTo.nonEmpty => // negation tends to get pretty verbose - if (beBrief) WildcardExample else NegativeExample(nonTrivialNonEqualTo) + if (beBrief) WildcardExample + else { + val eqTo = equalTo.headOption getOrElse TypeConst(variable.staticTpCheckable) + NegativeExample(eqTo, nonTrivialNonEqualTo) + } // not a valid counter-example, possibly since we have a definite type but there was a field mismatch // TODO: improve reasoning -- in the mean time, a false negative is better than an annoying false positive case _ => NoExample } - // patmatDebug("described as: "+ res) + // patmatDebug ("described as: "+ res) res } @@ -2748,16 +2750,16 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL val cond = test.cond def simplify(c: Cond): Set[Cond] = c match { - case AndCond(a, b) => simplify(a) ++ simplify(b) - case OrCond(_, _) => Set(Havoc) // TODO: supremum? - case NonNullCond(_) => Set(Top) // not worth remembering - case _ => Set(c) + case AndCond(a, b) => simplify(a) ++ simplify(b) + case OrCond(_, _) => Set(FalseCond) // TODO: make more precise + case NonNullCond(_) => Set(TrueCond) // not worth remembering + case _ => Set(c) } val conds = simplify(cond) - if (conds(Havoc)) false // stop when we encounter a havoc + if (conds(FalseCond)) false // stop when we encounter a definite "no" or a "not sure" else { - val nonTrivial = conds filterNot (_ == Top) + val nonTrivial = conds filterNot (_ == TrueCond) if (nonTrivial nonEmpty) { tested ++= nonTrivial @@ -2784,7 +2786,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL tested.clear() tests dropWhile storeDependencies } - // patmatDebug("dependencies: "+ dependencies) + // patmatDebug ("dependencies: "+ dependencies) // find longest prefix of tests that reuse a prior test, and whose dependent conditions monotonically increase // then, collapse these contiguous sequences of reusing tests @@ -2802,7 +2804,7 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // if there's no sharing, simply map to the tree makers corresponding to the tests var currDeps = Set[Cond]() val (sharedPrefix, suffix) = tests span { test => - (test.cond eq Top) || (for( + (test.cond == TrueCond) || (for( reusedTest <- test.reuses; nextDeps <- dependencies.get(reusedTest); diff <- (nextDeps -- currDeps).headOption; @@ -2819,23 +2821,23 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL } // patmatDebug("sharedPrefix: "+ sharedPrefix) - // if the shared prefix contains interesting conditions (!= Top) + // if the shared prefix contains interesting conditions (!= TrueCond) // and the last of such interesting shared conditions reuses another treemaker's test // replace the whole sharedPrefix by a ReusingCondTreeMaker - for (lastShared <- sharedPrefix.reverse.dropWhile(_.cond eq Top).headOption; + for (lastShared <- sharedPrefix.reverse.dropWhile(_.cond == TrueCond).headOption; lastReused <- lastShared.reuses) yield ReusingCondTreeMaker(sharedPrefix, reusedOrOrig) :: suffix.map(_.treeMaker) } - collapsedTreeMakers getOrElse tests.map(_.treeMaker) // sharedPrefix need not be empty (but it only contains Top-tests, which are dropped above) + collapsedTreeMakers getOrElse tests.map(_.treeMaker) // sharedPrefix need not be empty (but it only contains TrueCond-tests, which are dropped above) } okToCall = true // TODO: remove (debugging) // replace original treemakers that are reused (as determined when computing collapsed), // by ReusedCondTreeMakers val reusedMakers = collapsed mapConserve (_ mapConserve reusedOrOrig) -// patmatDebug("after CSE:") -// showTreeMakers(reusedMakers) + // patmatDebug ("after CSE:") + showTreeMakers(reusedMakers) reusedMakers } @@ -2918,55 +2920,197 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def alternativesSupported: Boolean + // when collapsing guarded switch cases we may sometimes need to jump to the default case + // however, that's not supported in exception handlers, so when we can't jump when we need it, don't emit a switch + // TODO: make more fine-grained, as we don't always need to jump + def canJump: Boolean + + def unchecked: Boolean + + def isDefault(x: CaseDef): Boolean def defaultSym: Symbol def defaultBody: Tree - def defaultCase(scrutSym: Symbol = defaultSym, body: Tree = defaultBody): CaseDef + def defaultCase(scrutSym: Symbol = defaultSym, guard: Tree = EmptyTree, body: Tree = defaultBody): CaseDef private def sequence[T](xs: List[Option[T]]): Option[List[T]] = if (xs exists (_.isEmpty)) None else Some(xs.flatten) + object GuardAndBodyTreeMakers { + def unapply(tms: List[TreeMaker]): Option[(Tree, Tree)] = { + tms match { + case (btm@BodyTreeMaker(body, _)) :: Nil => Some((EmptyTree, btm.substitution(body))) + case (gtm@GuardTreeMaker(guard)) :: (btm@BodyTreeMaker(body, _)) :: Nil => Some((gtm.substitution(guard), btm.substitution(body))) + case _ => None + } + } + } + + private val defaultLabel: Symbol = NoSymbol.newLabel(freshName("default"), NoPosition) setFlag SYNTH_CASE + + /** Collapse guarded cases that switch on the same constant (the last case may be unguarded). + * + * `{case C if(G_i) => B_i | case C'_j if G'_j => B'_j}*` is rewritten to + * `case C => {if(G_i) B_i}*` ++ rewrite({case C'_j if G'_j => B'_j}*) + * + */ + private def collapseGuardedCases(cases: List[CaseDef]) = { + // requires(switchesOnSameConst.forall(caseChecksSameConst(switchesOnSameConst.head))) + def collapse(switchesOnSameConst: List[CaseDef]): List[CaseDef] = + if (switchesOnSameConst.tail.isEmpty && (switchesOnSameConst.head.guard == EmptyTree)) switchesOnSameConst + else { + val commonPattern = switchesOnSameConst.head.pat + + // jump to default case (either the user-supplied one or the synthetic one) + // unless we're collapsing the default case, then re-use the same body as the synthetic catchall (throwing a matcherror, rethrowing the exception) + val jumpToDefault: Tree = + if (!canJump || isDefault(CaseDef(commonPattern, EmptyTree, EmptyTree))) defaultBody + else Apply(Ident(defaultLabel), Nil) + + val guardedBody = switchesOnSameConst.foldRight(jumpToDefault){ + // the last case may be un-guarded (we know it's the last one since fold's accum == jumpToDefault) + // --> replace jumpToDefault by the un-guarded case's body + case (CaseDef(_, EmptyTree, b), `jumpToDefault`) => b + case (CaseDef(_, g, b), els) if g != EmptyTree => If(g, b, els) + // error: the un-guarded case did not come last + case _ => + return switchesOnSameConst + } + + // if the cases that we're going to collapse bind variables, + // must replace them by the single binder introduced by the collapsed case + val binders = switchesOnSameConst.collect{case CaseDef(x@Bind(_, _), _, _) if x.symbol != NoSymbol => x.symbol} + val (pat, guardedBodySubst) = + if (binders.isEmpty) (commonPattern, guardedBody) + else { + // create a single fresh binder to subsume the old binders (and their types) + // TODO: I don't think the binder's types can actually be different (due to checks in caseChecksSameConst) + // if they do somehow manage to diverge, the lub might not be precise enough and we could get a type error + val binder = freshSym(binders.head.pos, lub(binders.map(_.tpe))) + + // the patterns in switchesOnSameConst are equal (according to caseChecksSameConst) and modulo variable-binding + // we can thus safely pick the first one arbitrarily, provided we correct binding + val origPatWithoutBind = commonPattern match { + case Bind(b, orig) => orig + case o => o + } + // need to replace `defaultSym` as well -- it's used in `defaultBody` (see `jumpToDefault` above) + val unifiedBody = guardedBody.substituteSymbols(defaultSym :: binders, binder :: binders.map(_ => binder)) + (Bind(binder, origPatWithoutBind), unifiedBody) + } + + List(CaseDef(pat, EmptyTree, guardedBodySubst)) + } + + @annotation.tailrec def partitionAndCollapse(cases: List[CaseDef], accum: List[CaseDef] = Nil): List[CaseDef] = + if (cases.isEmpty) accum + else { + val (same, others) = cases.tail partition (caseChecksSameConst(cases.head)) + partitionAndCollapse(others, accum ++ collapse(cases.head :: same)) + } + + // common case: no rewrite needed when there are no guards + if (cases.forall(c => c.guard == EmptyTree)) cases + else partitionAndCollapse(cases) + } + + private def caseChecksSameConst(x: CaseDef)(y: CaseDef) = (x, y) match { + // regular switch + case (CaseDef(Literal(Constant(cx)), _, _), CaseDef(Literal(Constant(cy)), _, _)) => cx == cy + case (CaseDef(Ident(nme.WILDCARD), _, _), CaseDef(Ident(nme.WILDCARD), _, _)) => true + // type-switch for catch + case (CaseDef(Bind(_, Typed(Ident(nme.WILDCARD), tpX)), _, _), CaseDef(Bind(_, Typed(Ident(nme.WILDCARD), tpY)), _, _)) => tpX.tpe =:= tpY.tpe + case _ => false + } + + private def checkNoGuards(cs: List[CaseDef]) = + if (cs.exists{case CaseDef(_, g, _) => g != EmptyTree case _ => false}) None + else Some(cs) + + // requires(cs.forall(_.guard == EmptyTree)) + private def unreachableCase(cs: List[CaseDef]): Option[CaseDef] = { + var cases = cs + var unreachable: Option[CaseDef] = None + + while (cases.nonEmpty && unreachable.isEmpty) { + if (isDefault(cases.head) && cases.tail.nonEmpty) unreachable = Some(cases.tail.head) + else unreachable = cases.tail.find(caseChecksSameConst(cases.head)) + + cases = cases.tail + } + + unreachable + } + // empty list ==> failure def apply(cases: List[(Symbol, List[TreeMaker])], pt: Type): List[CaseDef] = { val caseDefs = cases map { case (scrutSym, makers) => makers match { // default case - case (btm@BodyTreeMaker(body, _)) :: Nil => - Some(defaultCase(scrutSym, btm.substitution(body))) + case GuardAndBodyTreeMakers(guard, body) => + Some(defaultCase(scrutSym, guard, body)) // constant (or typetest for typeSwitch) - case SwitchableTreeMaker(pattern) :: (btm@BodyTreeMaker(body, _)) :: Nil => - Some(CaseDef(pattern, EmptyTree, btm.substitution(body))) + case SwitchableTreeMaker(pattern) :: GuardAndBodyTreeMakers(guard, body) => + Some(CaseDef(pattern, guard, body)) // alternatives - case AlternativesTreeMaker(_, altss, _) :: (btm@BodyTreeMaker(body, _)) :: Nil if alternativesSupported => - val casePatterns = altss map { + case AlternativesTreeMaker(_, altss, _) :: GuardAndBodyTreeMakers(guard, body) if alternativesSupported => + val switchableAlts = altss map { case SwitchableTreeMaker(pattern) :: Nil => Some(pattern) case _ => None } - sequence(casePatterns) map { patterns => - val substedBody = btm.substitution(body) - CaseDef(Alternative(patterns), EmptyTree, substedBody) + // succeed if they were all switchable + sequence(switchableAlts) map { switchableAlts => + CaseDef(Alternative(switchableAlts), guard, body) } - case _ => // patmatDebug("can't emit switch for "+ makers) + case _ => + // patmatDebug("can't emit switch for "+ makers) None //failure (can't translate pattern to a switch) } } (for( - caseDefs <- sequence(caseDefs)) yield - if (caseDefs exists isDefault) caseDefs - else { - caseDefs :+ defaultCase() + caseDefsWithGuards <- sequence(caseDefs); + collapsed = collapseGuardedCases(caseDefsWithGuards); + caseDefs <- checkNoGuards(collapsed)) yield { + if (!unchecked) + unreachableCase(caseDefs) foreach (cd => reportUnreachable(cd.body.pos)) + + // if we rewrote, we may need the default label to jump there (but then again, we may not) + // TODO: make more precise; we don't need the default label if + // - all collapsed cases included an un-guarded case (some of the guards of each case will always be true) + // - or: there was no default case (if all the guards of a case fail, it's a matcherror for sure) + val needDefaultLabel = (collapsed != caseDefsWithGuards) + + def wrapInDefaultLabelDef(cd: CaseDef): CaseDef = + if (needDefaultLabel && canJump) deriveCaseDef(cd){ b => + // TODO: can b.tpe ever be null? can't really use pt, see e.g. pos/t2683 or cps/match1.scala + defaultLabel setInfo MethodType(Nil, if (b.tpe != null) b.tpe else pt) + LabelDef(defaultLabel, Nil, b) + } else cd + + (caseDefs partition isDefault) match { + case (Nil, caseDefs) => caseDefs :+ wrapInDefaultLabelDef(defaultCase()) + case (default :: Nil, caseDefs) if canJump || !needDefaultLabel => + // we either didn't collapse (and thus definitely didn't have to emit a jump), + // or we canJump (and then the potential jumps in collapsed are ok) + caseDefs :+ wrapInDefaultLabelDef(default) + case _ => Nil + // TODO: if (canJump) error message (but multiple defaults should be caught by unreachability) + // if (!canJump) we got ourselves in the situation where we might need to emit a jump when we can't (in exception handler) + // --> TODO: refine the condition to detect whether we actually really needed to jump, but this seems relatively rare } + } ) getOrElse Nil } } - class RegularSwitchMaker(scrutSym: Symbol, matchFailGenOverride: Option[Tree => Tree]) extends SwitchMaker { + class RegularSwitchMaker(scrutSym: Symbol, matchFailGenOverride: Option[Tree => Tree], val unchecked: Boolean) extends SwitchMaker { val switchableTpe = Set(ByteClass.tpe, ShortClass.tpe, IntClass.tpe, CharClass.tpe) val alternativesSupported = true + val canJump = true object SwitchablePattern { def unapply(pat: Tree): Option[Tree] = pat match { case Literal(const@Constant((_: Byte ) | (_: Short) | (_: Int ) | (_: Char ))) => @@ -2988,13 +3132,13 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL def defaultSym: Symbol = scrutSym def defaultBody: Tree = { import CODE._; matchFailGenOverride map (gen => gen(REF(scrutSym))) getOrElse MATCHERROR(REF(scrutSym)) } - def defaultCase(scrutSym: Symbol = defaultSym, body: Tree = defaultBody): CaseDef = { import CODE._; atPos(body.pos) { - DEFAULT ==> body + def defaultCase(scrutSym: Symbol = defaultSym, guard: Tree = EmptyTree, body: Tree = defaultBody): CaseDef = { import CODE._; atPos(body.pos) { + (DEFAULT IF guard) ==> body }} } - override def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Tree => Tree]): Option[Tree] = { import CODE._ - val regularSwitchMaker = new RegularSwitchMaker(scrutSym, matchFailGenOverride) + override def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Tree => Tree], unchecked: Boolean): Option[Tree] = { import CODE._ + val regularSwitchMaker = new RegularSwitchMaker(scrutSym, matchFailGenOverride, unchecked) // TODO: if patterns allow switch but the type of the scrutinee doesn't, cast (type-test) the scrutinee to the corresponding switchable type and switch on the result if (regularSwitchMaker.switchableTpe(scrutSym.tpe)) { val caseDefsWithDefault = regularSwitchMaker(cases map {c => (scrutSym, c)}, pt) @@ -3014,8 +3158,10 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL // for the catch-cases in a try/catch private object typeSwitchMaker extends SwitchMaker { + val unchecked = false def switchableTpe(tp: Type) = true val alternativesSupported = false // TODO: needs either back-end support of flattening of alternatives during typers + val canJump = false // TODO: there are more treemaker-sequences that can be handled by type tests // analyze the result of approximateTreeMaker rather than the TreeMaker itself @@ -3037,8 +3183,8 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL lazy val defaultSym: Symbol = freshSym(NoPosition, ThrowableClass.tpe) def defaultBody: Tree = Throw(CODE.REF(defaultSym)) - def defaultCase(scrutSym: Symbol = defaultSym, body: Tree = defaultBody): CaseDef = { import CODE._; atPos(body.pos) { - CASE (Bind(scrutSym, Typed(Ident(nme.WILDCARD), TypeTree(ThrowableClass.tpe)))) ==> body + def defaultCase(scrutSym: Symbol = defaultSym, guard: Tree = EmptyTree, body: Tree = defaultBody): CaseDef = { import CODE._; atPos(body.pos) { + (CASE (Bind(scrutSym, Typed(Ident(nme.WILDCARD), TypeTree(ThrowableClass.tpe)))) IF guard) ==> body }} } @@ -3082,34 +3228,34 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL matchEnd setInfo MethodType(List(matchRes), restpe) def newCaseSym = NoSymbol.newLabel(freshName("case"), NoPosition) setInfo MethodType(Nil, restpe) setFlag SYNTH_CASE - var nextCase = newCaseSym - def caseDef(mkCase: Casegen => Tree): Tree = { - val currCase = nextCase - nextCase = newCaseSym - val casegen = new OptimizedCasegen(matchEnd, nextCase, restpe) - LabelDef(currCase, Nil, mkCase(casegen)) + var _currCase = newCaseSym + + val caseDefs = cases map { (mkCase: Casegen => Tree) => + val currCase = _currCase + val nextCase = newCaseSym + _currCase = nextCase + + LabelDef(currCase, Nil, mkCase(new OptimizedCasegen(matchEnd, nextCase, restpe))) } - def catchAll = matchFailGen map { matchFailGen => - val scrutRef = if(scrutSym ne NoSymbol) REF(scrutSym) else EmptyTree // for alternatives - // must jump to matchEnd, use result generated by matchFailGen (could be `FALSE` for isDefinedAt) - LabelDef(nextCase, Nil, matchEnd APPLY (matchFailGen(scrutRef))) - // don't cast the arg to matchEnd when using PartialFun synth in uncurry, since it won't detect the throw (see gen.withDefaultCase) - // the cast is necessary when using typedMatchAnonFun-style PartialFun synth: - // (_asInstanceOf(matchFailGen(scrutRef), restpe)) - } toList + // must compute catchAll after caseLabels (side-effects nextCase) // catchAll.isEmpty iff no synthetic default case needed (the (last) user-defined case is a default) // if the last user-defined case is a default, it will never jump to the next case; it will go immediately to matchEnd + val catchAllDef = matchFailGen map { matchFailGen => + val scrutRef = if(scrutSym ne NoSymbol) REF(scrutSym) else EmptyTree // for alternatives + + LabelDef(_currCase, Nil, matchEnd APPLY (matchFailGen(scrutRef))) + } toList // at most 1 element + + // scrutSym == NoSymbol when generating an alternatives matcher + val scrutDef = if(scrutSym ne NoSymbol) List(VAL(scrutSym) === scrut) else Nil // for alternatives // the generated block is taken apart in TailCalls under the following assumptions // the assumption is once we encounter a case, the remainder of the block will consist of cases // the prologue may be empty, usually it is the valdef that stores the scrut // val (prologue, cases) = stats span (s => !s.isInstanceOf[LabelDef]) - - // scrutSym == NoSymbol when generating an alternatives matcher - val scrutDef = if(scrutSym ne NoSymbol) List(VAL(scrutSym) === scrut) else Nil // for alternatives Block( - scrutDef ++ (cases map caseDef) ++ catchAll, + scrutDef ++ caseDefs ++ catchAllDef, LabelDef(matchEnd, List(matchRes), REF(matchRes)) ) } @@ -3179,16 +3325,11 @@ trait PatternMatching extends Transform with TypingTransformers with ast.TreeDSL override def optimizeCases(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type, unchecked: Boolean): (List[List[TreeMaker]], List[Tree]) = { if (!unchecked) { unreachableCase(prevBinder, cases, pt) foreach { caseIndex => - typer.context.unit.warning(cases(caseIndex).last.pos, "unreachable code") + reportUnreachable(cases(caseIndex).last.pos) } - } - val counterExamples = if (unchecked) Nil else exhaustive(prevBinder, cases, pt) - if (counterExamples.nonEmpty) { - val ceString = - if (counterExamples.tail.isEmpty) "input: " + counterExamples.head - else "inputs: " + counterExamples.mkString(", ") - - typer.context.unit.warning(prevBinder.pos, "match may not be exhaustive.\nIt would fail on the following "+ ceString) + val counterExamples = exhaustive(prevBinder, cases, pt) + if (counterExamples.nonEmpty) + reportMissingCases(prevBinder.pos, counterExamples) } val optCases = doCSE(prevBinder, doDCE(prevBinder, cases, pt), pt) |