From a0ec2dc1d5d6299409b4273125ead1f9ab57cdf7 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 15 Feb 2023 13:37:28 +0000 Subject: [PATCH 01/16] Space: Move isSubspace cache to Space instance This is in preparation for making SpaceEngine an object, so also remove the other vals too. --- .../dotty/tools/dotc/core/Definitions.scala | 3 ++ .../tools/dotc/transform/patmat/Space.scala | 53 ++++++++++--------- .../transform/patmat/SpaceEngineTest.scala | 2 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 854608143df9..d6e677547583 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -530,9 +530,12 @@ class Definitions { }) @tu lazy val ListClass: Symbol = requiredClass("scala.collection.immutable.List") + def ListType: TypeRef = ListClass.typeRef @tu lazy val ListModule: Symbol = requiredModule("scala.collection.immutable.List") @tu lazy val NilModule: Symbol = requiredModule("scala.collection.immutable.Nil") + def NilType: TermRef = NilModule.termRef @tu lazy val ConsClass: Symbol = requiredClass("scala.collection.immutable.::") + def ConsType: TypeRef = ConsClass.typeRef @tu lazy val SeqFactoryClass: Symbol = requiredClass("scala.collection.SeqFactory") @tu lazy val SingletonClass: ClassSymbol = diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index f4c4863d073d..7071e3b46829 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -55,7 +55,16 @@ import collection.mutable /** space definition */ -sealed trait Space +sealed trait Space: + private val isSubspaceCache = mutable.HashMap.empty[Space, Boolean] + + def isSubspace(b: Space)(engine: SpaceEngine)(using Context): Boolean = + if this == Empty then true + else if b == Empty then false + else trace(s"isSubspace(${engine.show(this)}, ${engine.show(b)})", debug) { + isSubspaceCache.getOrElseUpdate(b, engine.computeIsSubspace(this, b)) + } +end Space /** Empty space */ case object Empty extends Space @@ -349,13 +358,6 @@ object SpaceEngine { class SpaceEngine(using Context) extends SpaceLogic { import tpd._ - private val scalaSeqFactoryClass = defn.SeqFactoryClass - private val scalaListType = defn.ListClass.typeRef - private val scalaNilType = defn.NilModule.termRef - private val scalaConsType = defn.ConsClass.typeRef - - private val constantNullType = ConstantType(Constant(null)) - override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) { // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1). if !ctx.mode.is(Mode.SafeNulls) && (tp1.isNullType || tp2.isNullType) then @@ -398,7 +400,7 @@ class SpaceEngine(using Context) extends SpaceLogic { val funRef = fun1.tpe.asInstanceOf[TermRef] if (fun.symbol.name == nme.unapplySeq) val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos) - if (fun.symbol.owner == scalaSeqFactoryClass && scalaListType.appliedTo(elemTp) <:< pat.tpe) + if (fun.symbol.owner == defn.SeqFactoryClass && defn.ListType.appliedTo(elemTp) <:< pat.tpe) // The exhaustivity and reachability logic already handles decomposing sum types (into its subclasses) // and product types (into its components). To get better counter-examples for patterns that are of type // List (or a super-type of list, like LinearSeq) we project them into spaces that use `::` and Nil. @@ -519,16 +521,16 @@ class SpaceEngine(using Context) extends SpaceLogic { /** Space of the pattern: unapplySeq(a, b, c: _*) */ def projectSeq(pats: List[Tree]): Space = { - if (pats.isEmpty) return Typ(scalaNilType, false) + if (pats.isEmpty) return Typ(defn.NilType, false) val (items, zero) = if (isWildcardStarArg(pats.last)) - (pats.init, Typ(scalaListType.appliedTo(pats.last.tpe.elemType), false)) + (pats.init, Typ(defn.ListType.appliedTo(pats.last.tpe.elemType), false)) else - (pats, Typ(scalaNilType, false)) + (pats, Typ(defn.NilType, false)) - val unapplyTp = scalaConsType.classSymbol.companionModule.termRef.select(nme.unapply) + val unapplyTp = defn.ConsType.classSymbol.companionModule.termRef.select(nme.unapply) items.foldRight[Space](zero) { (pat, acc) => - val consTp = scalaConsType.appliedTo(pats.head.tpe.widen) + val consTp = defn.ConsType.appliedTo(pats.head.tpe.widen) Prod(consTp, unapplyTp, project(pat) :: acc :: Nil) } } @@ -538,13 +540,14 @@ class SpaceEngine(using Context) extends SpaceLogic { private val isSubspaceCache = mutable.HashMap.empty[(Space, Space, Context), Boolean] - override def isSubspace(a: Space, b: Space)(using Context): Boolean = - isSubspaceCache.getOrElseUpdate((a, b, ctx), super.isSubspace(a, b)) + override def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b)(this) + + def computeIsSubspace(a: Space, b: Space)(using Context): Boolean = super.isSubspace(a, b) /** Is `tp1` a subtype of `tp2`? */ def isSubType(tp1: Type, tp2: Type): Boolean = trace(i"$tp1 <:< $tp2", debug, show = true) { - if tp1 == constantNullType && !ctx.mode.is(Mode.SafeNulls) - then tp2 == constantNullType + if tp1 == ConstantType(Constant(null)) && !ctx.mode.is(Mode.SafeNulls) + then tp2 == ConstantType(Constant(null)) else tp1 <:< tp2 } @@ -593,10 +596,10 @@ class SpaceEngine(using Context) extends SpaceLogic { if (isUnapplySeq) { val (arity, elemTp, resultTp) = unapplySeqInfo(resTp, unappSym.srcPos) - if (elemTp.exists) scalaListType.appliedTo(elemTp) :: Nil + if (elemTp.exists) defn.ListType.appliedTo(elemTp) :: Nil else { val sels = productSeqSelectors(resultTp, arity, unappSym.srcPos) - sels.init :+ scalaListType.appliedTo(sels.last) + sels.init :+ defn.ListType.appliedTo(sels.last) } } else { @@ -821,7 +824,7 @@ class SpaceEngine(using Context) extends SpaceLogic { case Empty => "empty" case Typ(c: ConstantType, _) => "" + c.value.value case Typ(tp: TermRef, _) => - if (flattenList && tp <:< scalaNilType) "" + if (flattenList && tp <:< defn.NilType) "" else tp.symbol.showName case Typ(tp, decomposed) => @@ -829,9 +832,9 @@ class SpaceEngine(using Context) extends SpaceLogic { if (ctx.definitions.isTupleNType(tp)) params(tp).map(_ => "_").mkString("(", ", ", ")") - else if (scalaListType.isRef(sym)) + else if (defn.ListType.isRef(sym)) if (flattenList) "_*" else "_: List" - else if (scalaConsType.isRef(sym)) + else if (defn.ConsType.isRef(sym)) if (flattenList) "_, _*" else "List(_, _*)" else if (tp.classSymbol.is(Sealed) && tp.classSymbol.hasAnonymousChild) "_: " + showType(tp) + " (anonymous)" @@ -843,7 +846,7 @@ class SpaceEngine(using Context) extends SpaceLogic { case Prod(tp, fun, params) => if (ctx.definitions.isTupleNType(tp)) "(" + params.map(doShow(_)).mkString(", ") + ")" - else if (tp.isRef(scalaConsType.symbol)) + else if (tp.isRef(defn.ConsType.symbol)) if (flattenList) params.map(doShow(_, flattenList)).filter(_.nonEmpty).mkString(", ") else params.map(doShow(_, flattenList = true)).filter(!_.isEmpty).mkString("List(", ", ", ")") else { @@ -961,7 +964,7 @@ class SpaceEngine(using Context) extends SpaceLogic { val isNullable = selTyp.classSymbol.isNullableClass val targetSpace = if isNullable - then project(OrType(selTyp, constantNullType, soft = false)) + then project(OrType(selTyp, ConstantType(Constant(null)), soft = false)) else project(selTyp) debug.println(s"targetSpace: ${show(targetSpace)}") diff --git a/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala b/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala index 699b36caa508..13dfb7af668a 100644 --- a/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala +++ b/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala @@ -20,7 +20,7 @@ class SpaceEngineTest: val engine = patmat.SpaceEngine() import engine.* - val tp = defn.ConsClass.typeRef.appliedTo(defn.AnyType) + val tp = defn.ConsType.appliedTo(defn.AnyType) val unappTp = requiredMethod("scala.collection.immutable.::.unapply").termRef val params = List(Empty, Typ(tp)) From 5a21922cb75d9dafde9a5f1cb8b1a8ab97f33e44 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 9 Jan 2023 17:29:16 +0000 Subject: [PATCH 02/16] Space: Remove SpaceLogic and make SpaceEngine stateless --- .../tools/dotc/transform/PatternMatcher.scala | 13 +- .../tools/dotc/transform/patmat/Space.scala | 136 +++++++----------- .../transform/patmat/SpaceEngineTest.scala | 4 +- 3 files changed, 60 insertions(+), 93 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 63ffdffbddef..82828078e586 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -2,11 +2,10 @@ package dotty.tools package dotc package transform -import scala.annotation.tailrec import core._ import MegaPhase._ -import collection.mutable import Symbols._, Contexts._, Types._, StdNames._, NameOps._ +import patmat.SpaceEngine import util.Spans._ import typer.Applications.* import SymUtils._ @@ -16,9 +15,12 @@ import Decorators._ import NameKinds.{PatMatStdBinderName, PatMatAltsName, PatMatResultName} import config.Printers.patmatch import reporting._ -import dotty.tools.dotc.ast._ +import ast._ import util.Property._ +import scala.annotation.tailrec +import scala.collection.mutable + /** The pattern matching transform. * After this phase, the only Match nodes remaining in the code are simple switches * where every pattern is an integer or string constant @@ -45,9 +47,8 @@ class PatternMatcher extends MiniPhase { val translated = new Translator(matchType, this).translateMatch(tree) // check exhaustivity and unreachability - val engine = new patmat.SpaceEngine - engine.checkExhaustivity(tree) - engine.checkRedundancy(tree) + SpaceEngine.checkExhaustivity(tree) + SpaceEngine.checkRedundancy(tree) translated.ensureConforms(matchType) } diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 7071e3b46829..b6da5f2dba9c 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -22,9 +22,10 @@ import transform.SymUtils._ import reporting._ import config.Printers.{exhaustivity => debug} import util.{SrcPos, NoSourcePosition} -import collection.mutable -/** Space logic for checking exhaustivity and unreachability of pattern matching +import scala.collection.mutable + +/* Space logic for checking exhaustivity and unreachability of pattern matching * * Space can be thought of as a set of possible values. A type or a pattern * both refer to spaces. The space of a type is the values that inhabit the @@ -53,16 +54,17 @@ import collection.mutable * */ - /** space definition */ sealed trait Space: + import SpaceEngine.* + private val isSubspaceCache = mutable.HashMap.empty[Space, Boolean] - def isSubspace(b: Space)(engine: SpaceEngine)(using Context): Boolean = + def isSubspace(b: Space)(using Context): Boolean = if this == Empty then true else if b == Empty then false - else trace(s"isSubspace(${engine.show(this)}, ${engine.show(b)})", debug) { - isSubspaceCache.getOrElseUpdate(b, engine.computeIsSubspace(this, b)) + else trace(s"isSubspace(${show(this)}, ${show(b)})", debug) { + isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(this, b)) } end Space @@ -83,44 +85,8 @@ case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space /** Union of spaces */ case class Or(spaces: Seq[Space]) extends Space -/** abstract space logic */ -trait SpaceLogic { - /** Is `tp1` a subtype of `tp2`? */ - def isSubType(tp1: Type, tp2: Type): Boolean - - /** True if we can assume that the two unapply methods are the same. - * That is, given the same parameter, they return the same result. - * - * We assume that unapply methods are pure, but the same method may - * be called with different prefixes, thus behaving differently. - */ - def isSameUnapply(tp1: TermRef, tp2: TermRef): Boolean - - /** Return a space containing the values of both types. - * - * The types should be atomic (non-decomposable) and unrelated (neither - * should be a subtype of the other). - */ - def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space - - /** Is the type `tp` decomposable? i.e. all values of the type can be covered - * by its decomposed types. - * - * Abstract sealed class, OrType, Boolean and Java enums can be decomposed. - */ - def canDecompose(tp: Type): Boolean - - /** Return term parameter types of the extractor `unapp` */ - def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int): List[Type] - - /** Get components of decomposable types */ - def decompose(tp: Type): List[Typ] - - /** Whether the extractor covers the given type */ - def covers(unapp: TermRef, scrutineeTp: Type, argLen: Int): Boolean - - /** Display space in string format */ - def show(sp: Space): String +object SpaceEngine { + import tpd._ /** Simplify space such that a space equal to `Empty` becomes `Empty` */ def simplify(space: Space)(using Context): Space = trace(s"simplify ${show(space)} --> ", debug, show)(space match { @@ -174,7 +140,9 @@ trait SpaceLogic { } /** Is `a` a subspace of `b`? Equivalent to `simplify(simplify(a) - simplify(b)) == Empty`, but faster */ - def isSubspace(a: Space, b: Space)(using Context): Boolean = trace(s"isSubspace(${show(a)}, ${show(b)})", debug) { + def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b) + + def computeIsSubspace(a: Space, b: Space)(using Context): Boolean = { def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b) def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp))) @@ -302,9 +270,6 @@ trait SpaceLogic { Or(spaces) } } -} - -object SpaceEngine { /** Is the unapply or unapplySeq irrefutable? * @param unapp The unapply function reference @@ -352,13 +317,13 @@ object SpaceEngine { case _ => false } -} - -/** Scala implementation of space logic */ -class SpaceEngine(using Context) extends SpaceLogic { - import tpd._ - override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) { + /** Return a space containing the values of both types. + * + * The types should be atomic (non-decomposable) and unrelated (neither + * should be a subtype of the other). + */ + def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type)(using Context): Space = trace(i"atomic intersection: ${AndType(tp1, tp2)}", debug) { // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1). if !ctx.mode.is(Mode.SafeNulls) && (tp1.isNullType || tp2.isNullType) then // Since projections of types don't include null, intersection with null is empty. @@ -373,7 +338,7 @@ class SpaceEngine(using Context) extends SpaceLogic { } /** Return the space that represents the pattern `pat` */ - def project(pat: Tree): Space = pat match { + def project(pat: Tree)(using Context): Space = pat match { case Literal(c) => if (c.value.isInstanceOf[Symbol]) Typ(c.value.asInstanceOf[Symbol].termRef, decomposed = false) @@ -436,12 +401,12 @@ class SpaceEngine(using Context) extends SpaceLogic { Typ(pat.tpe.narrow, decomposed = false) } - private def project(tp: Type): Space = tp match { + private def project(tp: Type)(using Context): Space = tp match { case OrType(tp1, tp2) => Or(project(tp1) :: project(tp2) :: Nil) case tp => Typ(tp, decomposed = true) } - private def unapplySeqInfo(resTp: Type, pos: SrcPos): (Int, Type, Type) = { + private def unapplySeqInfo(resTp: Type, pos: SrcPos)(using Context): (Int, Type, Type) = { var resultTp = resTp var elemTp = unapplySeqTypeElemTp(resultTp) var arity = productArity(resultTp, pos) @@ -488,7 +453,7 @@ class SpaceEngine(using Context) extends SpaceLogic { * If `isValue` is true, then pattern-bound symbols are erased to its upper bound. * This is needed to avoid spurious unreachable warnings. See tests/patmat/i6197.scala. */ - private def erase(tp: Type, inArray: Boolean = false, isValue: Boolean = false): Type = trace(i"$tp erased to", debug) { + private def erase(tp: Type, inArray: Boolean = false, isValue: Boolean = false)(using Context): Type = trace(i"$tp erased to", debug) { tp match { case tp @ AppliedType(tycon, args) if tycon.typeSymbol.isPatternBound => @@ -520,7 +485,7 @@ class SpaceEngine(using Context) extends SpaceLogic { /** Space of the pattern: unapplySeq(a, b, c: _*) */ - def projectSeq(pats: List[Tree]): Space = { + def projectSeq(pats: List[Tree])(using Context): Space = { if (pats.isEmpty) return Typ(defn.NilType, false) val (items, zero) = if (isWildcardStarArg(pats.last)) @@ -535,29 +500,30 @@ class SpaceEngine(using Context) extends SpaceLogic { } } - def isPrimToBox(tp: Type, pt: Type): Boolean = + def isPrimToBox(tp: Type, pt: Type)(using Context): Boolean = tp.isPrimitiveValueType && (defn.boxedType(tp).classSymbol eq pt.classSymbol) - private val isSubspaceCache = mutable.HashMap.empty[(Space, Space, Context), Boolean] - - override def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b)(this) - - def computeIsSubspace(a: Space, b: Space)(using Context): Boolean = super.isSubspace(a, b) - /** Is `tp1` a subtype of `tp2`? */ - def isSubType(tp1: Type, tp2: Type): Boolean = trace(i"$tp1 <:< $tp2", debug, show = true) { + def isSubType(tp1: Type, tp2: Type)(using Context): Boolean = trace(i"$tp1 <:< $tp2", debug, show = true) { if tp1 == ConstantType(Constant(null)) && !ctx.mode.is(Mode.SafeNulls) then tp2 == ConstantType(Constant(null)) else tp1 <:< tp2 } - def isSameUnapply(tp1: TermRef, tp2: TermRef): Boolean = + /** True if we can assume that the two unapply methods are the same. + * That is, given the same parameter, they return the same result. + * + * We assume that unapply methods are pure, but the same method may + * be called with different prefixes, thus behaving differently. + */ + def isSameUnapply(tp1: TermRef, tp2: TermRef)(using Context): Boolean = // always assume two TypeTest[S, T].unapply are the same if they are equal in types (tp1.prefix.isStable && tp2.prefix.isStable || tp1.symbol == defn.TypeTest_unapply) && tp1 =:= tp2 - /** Parameter types of the case class type `tp`. Adapted from `unapplyPlan` in patternMatcher */ - def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int): List[Type] = { + /** Return term parameter types of the extractor `unapp`. + * Parameter types of the case class type `tp`. Adapted from `unapplyPlan` in patternMatcher */ + def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = { val unappSym = unapp.symbol // println("scrutineeTp = " + scrutineeTp.show) @@ -620,14 +586,14 @@ class SpaceEngine(using Context) extends SpaceLogic { } /** Whether the extractor covers the given type */ - def covers(unapp: TermRef, scrutineeTp: Type, argLen: Int): Boolean = + def covers(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): Boolean = SpaceEngine.isIrrefutable(unapp, argLen) || unapp.symbol == defn.TypeTest_unapply && { val AppliedType(_, _ :: tp :: Nil) = unapp.prefix.widen.dealias: @unchecked scrutineeTp <:< tp } /** Decompose a type into subspaces -- assume the type can be decomposed */ - def decompose(tp: Type): List[Typ] = trace(i"decompose($tp)", debug, show(_: Seq[Space])) { + def decompose(tp: Type)(using Context): List[Typ] = trace(i"decompose($tp)", debug, showSpaces) { def rec(tp: Type, mixins: List[Type]): List[Typ] = tp.dealias match { case AndType(tp1, tp2) => def decomposeComponent(tpA: Type, tpB: Type): List[Typ] = @@ -708,7 +674,7 @@ class SpaceEngine(using Context) extends SpaceLogic { } /** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */ - def canDecompose(tp: Type): Boolean = + def canDecompose(tp: Type)(using Context): Boolean = val res = tp.dealias match case AppliedType(tycon, _) if canDecompose(tycon) => true case tp: NamedType if canDecompose(tp.prefix) => true @@ -735,7 +701,7 @@ class SpaceEngine(using Context) extends SpaceLogic { * C --> C if current owner is C !!! * */ - def showType(tp: Type, showTypeArgs: Boolean = false): String = { + def showType(tp: Type, showTypeArgs: Boolean = false)(using Context): String = { val enclosingCls = ctx.owner.enclosingClass def isOmittable(sym: Symbol) = @@ -776,7 +742,7 @@ class SpaceEngine(using Context) extends SpaceLogic { } /** Whether the counterexample is satisfiable. The space is flattened and non-empty. */ - def satisfiable(sp: Space): Boolean = { + def satisfiable(sp: Space)(using Context): Boolean = { def impossible: Nothing = throw new AssertionError("`satisfiable` only accepts flattened space.") def genConstraint(space: Space): List[(Type, Type)] = space match { @@ -807,10 +773,10 @@ class SpaceEngine(using Context) extends SpaceLogic { checkConstraint(genConstraint(sp))(using ctx.fresh.setNewTyperState()) } - def show(ss: Seq[Space]): String = ss.map(show).mkString(", ") + def showSpaces(ss: Seq[Space])(using Context): String = ss.map(show).mkString(", ") /** Display spaces */ - def show(s: Space): String = { + def show(s: Space)(using Context): String = { def params(tp: Type): List[Type] = tp.classSymbol.primaryConstructor.info.firstParamTypes /** does the companion object of the given symbol have custom unapply */ @@ -862,7 +828,7 @@ class SpaceEngine(using Context) extends SpaceLogic { doShow(s, flattenList = false) } - private def exhaustivityCheckable(sel: Tree): Boolean = { + private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = { val seen = collection.mutable.Set.empty[Type] // Possible to check everything, but be compatible with scalac by default @@ -891,8 +857,8 @@ class SpaceEngine(using Context) extends SpaceLogic { res } - /** Whehter counter-examples should be further checked? True for GADTs. */ - private def shouldCheckExamples(tp: Type): Boolean = + /** Whether counter-examples should be further checked? True for GADTs. */ + private def shouldCheckExamples(tp: Type)(using Context): Boolean = new TypeAccumulator[Boolean] { override def apply(b: Boolean, tp: Type): Boolean = tp match { case tref: TypeRef if tref.symbol.is(TypeParam) && variance != 1 => true @@ -903,7 +869,7 @@ class SpaceEngine(using Context) extends SpaceLogic { /** Return the underlying type of non-module, non-constant, non-enum case singleton types. * Also widen ExprType to its result type, and rewrap any annotation wrappers. * For example, with `val opt = None`, widen `opt.type` to `None.type`. */ - def toUnderlying(tp: Type): Type = trace(i"toUnderlying($tp)", show = true)(tp match { + def toUnderlying(tp: Type)(using Context): Type = trace(i"toUnderlying($tp)", show = true)(tp match { case _: ConstantType => tp case tp: TermRef if tp.symbol.is(Module) => tp case tp: TermRef if tp.symbol.isAllOf(EnumCase) => tp @@ -913,7 +879,7 @@ class SpaceEngine(using Context) extends SpaceLogic { case _ => tp }) - def checkExhaustivity(_match: Match): Unit = { + def checkExhaustivity(_match: Match)(using Context): Unit = { val Match(sel, cases) = _match debug.println(i"checking exhaustivity of ${_match}") @@ -939,10 +905,10 @@ class SpaceEngine(using Context) extends SpaceLogic { if uncovered.nonEmpty then val hasMore = uncovered.lengthCompare(6) > 0 val deduped = dedup(uncovered.take(6)) - report.warning(PatternMatchExhaustivity(show(deduped), hasMore), sel.srcPos) + report.warning(PatternMatchExhaustivity(showSpaces(deduped), hasMore), sel.srcPos) } - private def redundancyCheckable(sel: Tree): Boolean = + private def redundancyCheckable(sel: Tree)(using Context): Boolean = // Ignore Expr[T] and Type[T] for unreachability as a special case. // Quote patterns produce repeated calls to the same unapply method, but with different implicit parameters. // Since we assume that repeated calls to the same unapply method overlap @@ -952,7 +918,7 @@ class SpaceEngine(using Context) extends SpaceLogic { && !sel.tpe.widen.isRef(defn.QuotedExprClass) && !sel.tpe.widen.isRef(defn.QuotedTypeClass) - def checkRedundancy(_match: Match): Unit = { + def checkRedundancy(_match: Match)(using Context): Unit = { val Match(sel, _) = _match val cases = _match.cases.toIndexedSeq debug.println(i"checking redundancy in $_match") diff --git a/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala b/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala index 13dfb7af668a..c13ef0532348 100644 --- a/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala +++ b/compiler/test/dotty/tools/dotc/transform/patmat/SpaceEngineTest.scala @@ -11,14 +11,14 @@ import vulpix.TestConfiguration, TestConfiguration.basicClasspath import org.junit, junit.Test, junit.Assert.* class SpaceEngineTest: + import SpaceEngine.* + @Test def isSubspaceTest1: Unit = inCompilerContext(basicClasspath) { // Testing the property of `isSubspace` that: // isSubspace(a, b) <=> simplify(simplify(a) - simplify(a)) == Empty // Previously there were no simplify calls, // and this is a counter-example, // for which you need either to simplify(b) or simplify the minus result. - val engine = patmat.SpaceEngine() - import engine.* val tp = defn.ConsType.appliedTo(defn.AnyType) val unappTp = requiredMethod("scala.collection.immutable.::.unapply").termRef From 53a782751ac5eb21d84b137e8d23d4d49bd422f0 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 9 Jan 2023 20:34:44 +0000 Subject: [PATCH 03/16] Space: dedupe into intersectUnrelatedAtomicTypes --- .../tools/dotc/transform/patmat/Space.scala | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index b6da5f2dba9c..d903d9e122a4 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -187,25 +187,19 @@ object SpaceEngine { else if (isSubType(tp2, tp1)) b else if (canDecompose(tp1)) tryDecompose1(tp1) else if (canDecompose(tp2)) tryDecompose2(tp2) - else intersectUnrelatedAtomicTypes(tp1, tp2) + else intersectUnrelatedAtomicTypes(tp1, tp2)(a) case (Typ(tp1, _), Prod(tp2, fun, ss)) => if (isSubType(tp2, tp1)) b else if (canDecompose(tp1)) tryDecompose1(tp1) else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class - else intersectUnrelatedAtomicTypes(tp1, tp2) match - case Typ(tp, _) => Prod(tp, fun, ss) - case sp => sp + else intersectUnrelatedAtomicTypes(tp1, tp2)(b) case (Prod(tp1, fun, ss), Typ(tp2, _)) => if (isSubType(tp1, tp2)) a else if (canDecompose(tp2)) tryDecompose2(tp2) else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class - else intersectUnrelatedAtomicTypes(tp1, tp2) match - case Typ(tp, _) => Prod(tp, fun, ss) - case sp => sp + else intersectUnrelatedAtomicTypes(tp1, tp2)(a) case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => - if (!isSameUnapply(fun1, fun2)) intersectUnrelatedAtomicTypes(tp1, tp2) match - case Typ(tp, _) => Prod(tp, fun1, ss1) - case sp => sp + if (!isSameUnapply(fun1, fun2)) intersectUnrelatedAtomicTypes(tp1, tp2)(a) else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty else Prod(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled)) } @@ -323,13 +317,15 @@ object SpaceEngine { * The types should be atomic (non-decomposable) and unrelated (neither * should be a subtype of the other). */ - def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type)(using Context): Space = trace(i"atomic intersection: ${AndType(tp1, tp2)}", debug) { + def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type)(sp: Space)(using Context): Space = trace(i"atomic intersection: ${AndType(tp1, tp2)}", debug) { // Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1). if !ctx.mode.is(Mode.SafeNulls) && (tp1.isNullType || tp2.isNullType) then // Since projections of types don't include null, intersection with null is empty. Empty else - val intersection = Typ(AndType(tp1, tp2), decomposed = false) + val intersection = sp match + case sp: Prod => sp.copy(AndType(tp1, tp2)) + case _ => Typ(AndType(tp1, tp2), decomposed = false) // unrelated numeric value classes can equal each other, so let's not consider type space intersection empty if tp1.classSymbol.isNumericValueClass && tp2.classSymbol.isNumericValueClass then intersection else if isPrimToBox(tp1, tp2) || isPrimToBox(tp2, tp1) then intersection From 23cee40af14f625c607e96e1bc31f092cc16715d Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 16 Feb 2023 10:58:36 +0000 Subject: [PATCH 04/16] Space: Prep for caching decompose/canDecompose on Typ --- .../tools/dotc/transform/patmat/Space.scala | 157 ++++++++---------- 1 file changed, 67 insertions(+), 90 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index d903d9e122a4..3003bea962ca 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -92,16 +92,16 @@ object SpaceEngine { def simplify(space: Space)(using Context): Space = trace(s"simplify ${show(space)} --> ", debug, show)(space match { case Prod(tp, fun, spaces) => val sps = spaces.mapconserve(simplify) - if (sps.contains(Empty)) Empty - else if (canDecompose(tp) && decompose(tp).isEmpty) Empty + if sps.contains(Empty) then Empty + else if canDecompose(tp) && decompose(tp).isEmpty then Empty else if sps eq spaces then space else Prod(tp, fun, sps) case Or(spaces) => val spaces2 = spaces.map(simplify).filter(_ != Empty) if spaces2.isEmpty then Empty else if spaces2.lengthIs == 1 then spaces2.head else if spaces2.corresponds(spaces)(_ eq _) then space else Or(spaces2) - case Typ(tp, _) => - if (canDecompose(tp) && decompose(tp).isEmpty) Empty + case typ: Typ => + if canDecompose(typ.tp) && decompose(typ.tp).isEmpty then Empty else space case _ => space }) @@ -143,25 +143,21 @@ object SpaceEngine { def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b) def computeIsSubspace(a: Space, b: Space)(using Context): Boolean = { - def tryDecompose1(tp: Type) = canDecompose(tp) && isSubspace(Or(decompose(tp)), b) - def tryDecompose2(tp: Type) = canDecompose(tp) && isSubspace(a, Or(decompose(tp))) - val a2 = simplify(a) val b2 = simplify(b) if (a ne a2) || (b ne b2) then isSubspace(a2, b2) else (a, b) match { case (Empty, _) => true case (_, Empty) => false - case (Or(ss), _) => - ss.forall(isSubspace(_, b)) - case (Typ(tp1, _), Typ(tp2, _)) => + case (Or(ss), _) => ss.forall(isSubspace(_, b)) + case (a @ Typ(tp1, _), Or(ss)) => // optimization: don't go to subtraction too early + ss.exists(isSubspace(a, _)) + || canDecompose(tp1) && isSubspace(Or(decompose(tp1)), b) + case (_, Or(_)) => simplify(minus(a, b)) == Empty + case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => isSubType(tp1, tp2) - || canDecompose(tp1) && tryDecompose1(tp1) - || canDecompose(tp2) && tryDecompose2(tp2) - case (Typ(tp1, _), Or(ss)) => // optimization: don't go to subtraction too early - ss.exists(isSubspace(a, _)) || tryDecompose1(tp1) - case (_, Or(_)) => - simplify(minus(a, b)) == Empty + || canDecompose(tp1) && isSubspace(Or(decompose(tp1)), b) + || canDecompose(tp2) && isSubspace(a, Or(decompose(tp2))) case (Prod(tp1, _, _), Typ(tp2, _)) => isSubType(tp1, tp2) case (Typ(tp1, _), Prod(tp2, fun, ss)) => @@ -169,90 +165,74 @@ object SpaceEngine { && covers(fun, tp1, ss.length) && isSubspace(Prod(tp2, fun, signature(fun, tp1, ss.length).map(Typ(_, false))), b) case (Prod(_, fun1, ss1), Prod(_, fun2, ss2)) => - isSameUnapply(fun1, fun2) && ss1.zip(ss2).forall((isSubspace _).tupled) + isSameUnapply(fun1, fun2) && ss1.lazyZip(ss2).forall(isSubspace) } } /** Intersection of two spaces */ def intersect(a: Space, b: Space)(using Context): Space = trace(s"${show(a)} & ${show(b)}", debug, show) { - def tryDecompose1(tp: Type) = intersect(Or(decompose(tp)), b) - def tryDecompose2(tp: Type) = intersect(a, Or(decompose(tp))) - (a, b) match { case (Empty, _) | (_, Empty) => Empty case (_, Or(ss)) => Or(ss.map(intersect(a, _)).filter(_ ne Empty)) case (Or(ss), _) => Or(ss.map(intersect(_, b)).filter(_ ne Empty)) - case (Typ(tp1, _), Typ(tp2, _)) => - if (isSubType(tp1, tp2)) a - else if (isSubType(tp2, tp1)) b - else if (canDecompose(tp1)) tryDecompose1(tp1) - else if (canDecompose(tp2)) tryDecompose2(tp2) + case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => + if isSubType(tp1, tp2) then a + else if isSubType(tp2, tp1) then b + else if canDecompose(tp1) then intersect(Or(decompose(tp1)), b) + else if canDecompose(tp2) then intersect(a, Or(decompose(tp2))) else intersectUnrelatedAtomicTypes(tp1, tp2)(a) - case (Typ(tp1, _), Prod(tp2, fun, ss)) => - if (isSubType(tp2, tp1)) b - else if (canDecompose(tp1)) tryDecompose1(tp1) - else if (isSubType(tp1, tp2)) a // problematic corner case: inheriting a case class + case (a @ Typ(tp1, _), Prod(tp2, fun, ss)) => + if isSubType(tp2, tp1) then b + else if canDecompose(tp1) then intersect(Or(decompose(tp1)), b) + else if isSubType(tp1, tp2) then a // problematic corner case: inheriting a case class else intersectUnrelatedAtomicTypes(tp1, tp2)(b) - case (Prod(tp1, fun, ss), Typ(tp2, _)) => - if (isSubType(tp1, tp2)) a - else if (canDecompose(tp2)) tryDecompose2(tp2) - else if (isSubType(tp2, tp1)) a // problematic corner case: inheriting a case class + case (Prod(tp1, fun, ss), b @ Typ(tp2, _)) => + if isSubType(tp1, tp2) then a + else if canDecompose(tp2) then intersect(a, Or(decompose(tp2))) + else if isSubType(tp2, tp1) then a // problematic corner case: inheriting a case class else intersectUnrelatedAtomicTypes(tp1, tp2)(a) - case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => - if (!isSameUnapply(fun1, fun2)) intersectUnrelatedAtomicTypes(tp1, tp2)(a) - else if (ss1.zip(ss2).exists(p => simplify(intersect(p._1, p._2)) == Empty)) Empty - else Prod(tp1, fun1, ss1.zip(ss2).map((intersect _).tupled)) + case (a @ Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => + if !isSameUnapply(fun1, fun2) then intersectUnrelatedAtomicTypes(tp1, tp2)(a) + else if ss1.lazyZip(ss2).exists((a, b) => simplify(intersect(a, b)) == Empty) then Empty + else Prod(tp1, fun1, ss1.lazyZip(ss2).map(intersect)) } } /** The space of a not covered by b */ def minus(a: Space, b: Space)(using Context): Space = trace(s"${show(a)} - ${show(b)}", debug, show) { - def tryDecompose1(tp: Type) = minus(Or(decompose(tp)), b) - def tryDecompose2(tp: Type) = minus(a, Or(decompose(tp))) - (a, b) match { case (Empty, _) => Empty case (_, Empty) => a - case (Typ(tp1, _), Typ(tp2, _)) => - if (isSubType(tp1, tp2)) Empty - else if (canDecompose(tp1)) tryDecompose1(tp1) - else if (canDecompose(tp2)) tryDecompose2(tp2) + case (Or(ss), _) => Or(ss.map(minus(_, b))) + case (_, Or(ss)) => ss.foldLeft(a)(minus) + case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => + if isSubType(tp1, tp2) then Empty + else if canDecompose(tp1) then minus(Or(decompose(tp1)), b) + else if canDecompose(tp2) then minus(a, Or(decompose(tp2))) else a - case (Typ(tp1, _), Prod(tp2, fun, ss)) => + case (a @ Typ(tp1, _), Prod(tp2, fun, ss)) => // rationale: every instance of `tp1` is covered by `tp2(_)` if isSubType(tp1, tp2) && covers(fun, tp1, ss.length) then minus(Prod(tp1, fun, signature(fun, tp1, ss.length).map(Typ(_, false))), b) - else if canDecompose(tp1) then - tryDecompose1(tp1) - else - a - case (Or(ss), _) => - Or(ss.map(minus(_, b))) - case (_, Or(ss)) => - ss.foldLeft(a)(minus) - case (Prod(tp1, fun, ss), Typ(tp2, _)) => + else if canDecompose(tp1) then minus(Or(decompose(tp1)), b) + else a + case (Prod(tp1, fun, ss), b @ Typ(tp2, _)) => // uncovered corner case: tp2 :< tp1, may happen when inheriting case class - if (isSubType(tp1, tp2)) - Empty - else if (simplify(a) == Empty) - Empty - else if (canDecompose(tp2)) - tryDecompose2(tp2) - else - a + if isSubType(tp1, tp2) then Empty + else if simplify(a) == Empty then Empty + else if canDecompose(tp2) then minus(a, Or(decompose(tp2))) + else a case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) - if (!isSameUnapply(fun1, fun2)) => a + if !isSameUnapply(fun1, fun2) => a case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) - if (fun1.symbol.name == nme.unapply && ss1.length != ss2.length) => a - case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => - - val range = (0 until ss1.size).toList + if fun1.symbol.name == nme.unapply && ss1.length != ss2.length => a + case (a @ Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => + val range = ss1.indices.toList val cache = Array.fill[Space | Null](ss2.length)(null) def sub(i: Int) = if cache(i) == null then cache(i) = minus(ss1(i), ss2(i)) cache(i).nn - end sub if range.exists(i => isSubspace(ss1(i), sub(i))) then a else if cache.forall(sub => isSubspace(sub.nn, Empty)) then Empty @@ -610,26 +590,26 @@ object SpaceEngine { else decomposeComponent(tp2, tp1) - case OrType(tp1, tp2) => List(Typ(tp1, true), Typ(tp2, true)) + case OrType(tp1, tp2) => List(Typ(tp1, decomposed = true), Typ(tp2, decomposed = true)) case tp if tp.isRef(defn.BooleanClass) => List( - Typ(ConstantType(Constant(true)), true), - Typ(ConstantType(Constant(false)), true) + Typ(ConstantType(Constant(true)), decomposed = true), + Typ(ConstantType(Constant(false)), decomposed = true) ) case tp if tp.isRef(defn.UnitClass) => - Typ(ConstantType(Constant(())), true) :: Nil + Typ(ConstantType(Constant(())), decomposed = true) :: Nil case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => - tp.classSymbol.children.map(sym => Typ(sym.termRef, true)) + tp.classSymbol.children.map(sym => Typ(sym.termRef, decomposed = true)) case tp @ AppliedType(tycon, targs) if tp.classSymbol.children.isEmpty && canDecompose(tycon) => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. // But this is guarded by `tp.classSymbol.children.isEmpty`, // meaning we'll decompose to the same class, just not the same type. // For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`. - rec(tycon, Nil).map(typ => Typ(tp.derivedAppliedType(typ.tp, targs))) + rec(tycon, Nil).map(typ => Typ(tp.derivedAppliedType(typ.tp, targs), decomposed = true)) case tp: NamedType if canDecompose(tp.prefix) => - rec(tp.prefix, Nil).map(typ => Typ(tp.derivedSelect(typ.tp))) + rec(tp.prefix, Nil).map(typ => Typ(tp.derivedSelect(typ.tp), decomposed = true)) case tp => def getChildren(sym: Symbol): List[Symbol] = @@ -641,30 +621,27 @@ object SpaceEngine { else List(child) } val children = getChildren(tp.classSymbol) - debug.println(s"candidates for ${tp.show} : [${children.map(_.show).mkString(", ")}]") + debug.println(i"candidates for $tp : $children") val parts = children.map { sym => val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym val refined = TypeOps.refineUsingParent(tp, sym1, mixins) + debug.println(i"$sym1 refined to $refined") - debug.println(sym1.show + " refined to " + refined.show) - - def inhabited(tp: Type): Boolean = - tp.dealias match { - case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2) - case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2) - case tp: RefinedType => inhabited(tp.parent) - case tp: TypeRef => inhabited(tp.prefix) - case _ => true - } + def inhabited(tp: Type): Boolean = tp.dealias match + case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2) + case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2) + case tp: RefinedType => inhabited(tp.parent) + case tp: TypeRef => inhabited(tp.prefix) + case _ => true - if (inhabited(refined)) refined + if inhabited(refined) then refined else NoType - } filter(_.exists) + }.filter(_.exists) - debug.println(s"${tp.show} decomposes to [${parts.map(_.show).mkString(", ")}]") + debug.println(i"$tp decomposes to $parts") - parts.map(Typ(_, true)) + parts.map(Typ(_, decomposed = true)) } rec(tp, Nil) } From e316f65a54e17c86bf06d83ff6c447f27a72c690 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 16 Feb 2023 11:07:05 +0000 Subject: [PATCH 05/16] Space: Cache decompose/canDecompose on Typ --- .../tools/dotc/transform/patmat/Space.scala | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 3003bea962ca..d10fb537cb3a 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -77,7 +77,18 @@ case object Empty extends Space * @param decomposed: does the space result from decomposition? Used for pretty print * */ -case class Typ(tp: Type, decomposed: Boolean = true) extends Space +case class Typ(tp: Type, decomposed: Boolean = true) extends Space: + private var myCanDecompose: java.lang.Boolean = _ + private var myDecompose: List[Typ] = _ + + def canDecompose(using Context): Boolean = + if myCanDecompose == null then myCanDecompose = SpaceEngine.canDecompose(tp) + myCanDecompose + + def decompose(using Context): List[Typ] = + if myDecompose == null then myDecompose = SpaceEngine.decompose(tp) + myDecompose +end Typ /** Space representing an extractor pattern */ case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space @@ -101,7 +112,7 @@ object SpaceEngine { else if spaces2.lengthIs == 1 then spaces2.head else if spaces2.corresponds(spaces)(_ eq _) then space else Or(spaces2) case typ: Typ => - if canDecompose(typ.tp) && decompose(typ.tp).isEmpty then Empty + if canDecompose(typ) && decompose(typ).isEmpty then Empty else space case _ => space }) @@ -152,12 +163,12 @@ object SpaceEngine { case (Or(ss), _) => ss.forall(isSubspace(_, b)) case (a @ Typ(tp1, _), Or(ss)) => // optimization: don't go to subtraction too early ss.exists(isSubspace(a, _)) - || canDecompose(tp1) && isSubspace(Or(decompose(tp1)), b) + || canDecompose(a) && isSubspace(Or(decompose(a)), b) case (_, Or(_)) => simplify(minus(a, b)) == Empty case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => isSubType(tp1, tp2) - || canDecompose(tp1) && isSubspace(Or(decompose(tp1)), b) - || canDecompose(tp2) && isSubspace(a, Or(decompose(tp2))) + || canDecompose(a) && isSubspace(Or(decompose(a)), b) + || canDecompose(b) && isSubspace(a, Or(decompose(b))) case (Prod(tp1, _, _), Typ(tp2, _)) => isSubType(tp1, tp2) case (Typ(tp1, _), Prod(tp2, fun, ss)) => @@ -178,17 +189,17 @@ object SpaceEngine { case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => if isSubType(tp1, tp2) then a else if isSubType(tp2, tp1) then b - else if canDecompose(tp1) then intersect(Or(decompose(tp1)), b) - else if canDecompose(tp2) then intersect(a, Or(decompose(tp2))) + else if canDecompose(a) then intersect(Or(decompose(a)), b) + else if canDecompose(b) then intersect(a, Or(decompose(b))) else intersectUnrelatedAtomicTypes(tp1, tp2)(a) case (a @ Typ(tp1, _), Prod(tp2, fun, ss)) => if isSubType(tp2, tp1) then b - else if canDecompose(tp1) then intersect(Or(decompose(tp1)), b) + else if canDecompose(a) then intersect(Or(decompose(a)), b) else if isSubType(tp1, tp2) then a // problematic corner case: inheriting a case class else intersectUnrelatedAtomicTypes(tp1, tp2)(b) case (Prod(tp1, fun, ss), b @ Typ(tp2, _)) => if isSubType(tp1, tp2) then a - else if canDecompose(tp2) then intersect(a, Or(decompose(tp2))) + else if canDecompose(b) then intersect(a, Or(decompose(b))) else if isSubType(tp2, tp1) then a // problematic corner case: inheriting a case class else intersectUnrelatedAtomicTypes(tp1, tp2)(a) case (a @ Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) => @@ -207,20 +218,20 @@ object SpaceEngine { case (_, Or(ss)) => ss.foldLeft(a)(minus) case (a @ Typ(tp1, _), b @ Typ(tp2, _)) => if isSubType(tp1, tp2) then Empty - else if canDecompose(tp1) then minus(Or(decompose(tp1)), b) - else if canDecompose(tp2) then minus(a, Or(decompose(tp2))) + else if canDecompose(a) then minus(Or(decompose(a)), b) + else if canDecompose(b) then minus(a, Or(decompose(b))) else a case (a @ Typ(tp1, _), Prod(tp2, fun, ss)) => // rationale: every instance of `tp1` is covered by `tp2(_)` if isSubType(tp1, tp2) && covers(fun, tp1, ss.length) then minus(Prod(tp1, fun, signature(fun, tp1, ss.length).map(Typ(_, false))), b) - else if canDecompose(tp1) then minus(Or(decompose(tp1)), b) + else if canDecompose(a) then minus(Or(decompose(a)), b) else a case (Prod(tp1, fun, ss), b @ Typ(tp2, _)) => // uncovered corner case: tp2 :< tp1, may happen when inheriting case class if isSubType(tp1, tp2) then Empty else if simplify(a) == Empty then Empty - else if canDecompose(tp2) then minus(a, Or(decompose(tp2))) + else if canDecompose(b) then minus(a, Or(decompose(b))) else a case (Prod(tp1, fun1, ss1), Prod(tp2, fun2, ss2)) if !isSameUnapply(fun1, fun2) => a @@ -568,6 +579,9 @@ object SpaceEngine { scrutineeTp <:< tp } + def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose + def decompose(typ: Typ)(using Context): List[Typ] = typ.decompose + /** Decompose a type into subspaces -- assume the type can be decomposed */ def decompose(tp: Type)(using Context): List[Typ] = trace(i"decompose($tp)", debug, showSpaces) { def rec(tp: Type, mixins: List[Type]): List[Typ] = tp.dealias match { From 512a3073ac7b0a46a7571dcfcce9f7dfd9c8509f Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 16 Feb 2023 11:44:24 +0000 Subject: [PATCH 06/16] Space: Move out Typ-wrapping in decompose --- .../tools/dotc/transform/patmat/Space.scala | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index d10fb537cb3a..de92a3c01076 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -584,19 +584,13 @@ object SpaceEngine { /** Decompose a type into subspaces -- assume the type can be decomposed */ def decompose(tp: Type)(using Context): List[Typ] = trace(i"decompose($tp)", debug, showSpaces) { - def rec(tp: Type, mixins: List[Type]): List[Typ] = tp.dealias match { + def rec(tp: Type, mixins: List[Type]): List[Type] = tp.dealias match { case AndType(tp1, tp2) => - def decomposeComponent(tpA: Type, tpB: Type): List[Typ] = - rec(tpA, tpB :: mixins).flatMap { - case Typ(tp, _) => - if tp <:< tpB then - Typ(tp, decomposed = true) :: Nil - else if tpB <:< tp then - Typ(tpB, decomposed = true) :: Nil - else if TypeComparer.provablyDisjoint(tp, tpB) then - Nil - else - Typ(AndType(tp, tpB), decomposed = true) :: Nil + def decomposeComponent(tpA: Type, tpB: Type): List[Type] = + rec(tpA, tpB :: mixins).collect { + case tp if tp <:< tpB => tp + case tp if tpB <:< tp => tpB + case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) } if canDecompose(tp1) then @@ -604,26 +598,26 @@ object SpaceEngine { else decomposeComponent(tp2, tp1) - case OrType(tp1, tp2) => List(Typ(tp1, decomposed = true), Typ(tp2, decomposed = true)) + case OrType(tp1, tp2) => List(tp1, tp2) case tp if tp.isRef(defn.BooleanClass) => List( - Typ(ConstantType(Constant(true)), decomposed = true), - Typ(ConstantType(Constant(false)), decomposed = true) + ConstantType(Constant(true)), + ConstantType(Constant(false)) ) case tp if tp.isRef(defn.UnitClass) => - Typ(ConstantType(Constant(())), decomposed = true) :: Nil + ConstantType(Constant(())) :: Nil case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => - tp.classSymbol.children.map(sym => Typ(sym.termRef, decomposed = true)) + tp.classSymbol.children.map(_.termRef) case tp @ AppliedType(tycon, targs) if tp.classSymbol.children.isEmpty && canDecompose(tycon) => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. // But this is guarded by `tp.classSymbol.children.isEmpty`, // meaning we'll decompose to the same class, just not the same type. // For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`. - rec(tycon, Nil).map(typ => Typ(tp.derivedAppliedType(typ.tp, targs), decomposed = true)) + rec(tycon, Nil).map(tp.derivedAppliedType(_, targs)) case tp: NamedType if canDecompose(tp.prefix) => - rec(tp.prefix, Nil).map(typ => Typ(tp.derivedSelect(typ.tp), decomposed = true)) + rec(tp.prefix, Nil).map(tp.derivedSelect) case tp => def getChildren(sym: Symbol): List[Symbol] = @@ -655,9 +649,9 @@ object SpaceEngine { debug.println(i"$tp decomposes to $parts") - parts.map(Typ(_, decomposed = true)) + parts } - rec(tp, Nil) + rec(tp, Nil).map(Typ(_, decomposed = true)) } /** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */ From 7f4009b891da72a3fe1fa3d44a16ba5f7105c761 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 16 Feb 2023 14:59:55 +0000 Subject: [PATCH 07/16] Space: Redefine canDecompose in terms of decompose --- .../tools/dotc/transform/patmat/Space.scala | 113 ++++++++---------- 1 file changed, 52 insertions(+), 61 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index de92a3c01076..cffe6a378649 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -78,15 +78,12 @@ case object Empty extends Space * */ case class Typ(tp: Type, decomposed: Boolean = true) extends Space: - private var myCanDecompose: java.lang.Boolean = _ private var myDecompose: List[Typ] = _ - def canDecompose(using Context): Boolean = - if myCanDecompose == null then myCanDecompose = SpaceEngine.canDecompose(tp) - myCanDecompose + def canDecompose(using Context): Boolean = decompose != SpaceEngine.ListOfTypNoType def decompose(using Context): List[Typ] = - if myDecompose == null then myDecompose = SpaceEngine.decompose(tp) + if myDecompose == null then myDecompose = SpaceEngine.decompose(tp).map(Typ(_, decomposed = true)) myDecompose end Typ @@ -104,7 +101,7 @@ object SpaceEngine { case Prod(tp, fun, spaces) => val sps = spaces.mapconserve(simplify) if sps.contains(Empty) then Empty - else if canDecompose(tp) && decompose(tp).isEmpty then Empty + else if decompose(tp).isEmpty then Empty else if sps eq spaces then space else Prod(tp, fun, sps) case Or(spaces) => val spaces2 = spaces.map(simplify).filter(_ != Empty) @@ -112,7 +109,7 @@ object SpaceEngine { else if spaces2.lengthIs == 1 then spaces2.head else if spaces2.corresponds(spaces)(_ eq _) then space else Or(spaces2) case typ: Typ => - if canDecompose(typ) && decompose(typ).isEmpty then Empty + if decompose(typ).isEmpty then Empty else space case _ => space }) @@ -583,43 +580,53 @@ object SpaceEngine { def decompose(typ: Typ)(using Context): List[Typ] = typ.decompose /** Decompose a type into subspaces -- assume the type can be decomposed */ - def decompose(tp: Type)(using Context): List[Typ] = trace(i"decompose($tp)", debug, showSpaces) { - def rec(tp: Type, mixins: List[Type]): List[Type] = tp.dealias match { + def decompose(tp: Type)(using Context): List[Type] = trace(i"decompose($tp)", debug) { + var lastType: Type = NoType + var lastParts: List[Type] = Nil + def dec(tp: Type) = + if tp eq lastType then lastParts + else + lastType = tp + lastParts = decompose(tp) + lastParts + def canDec(tp: Type) = + lastParts = dec(tp) + lastParts != ListOfNoType + + def rec(tp: Type, mixins: List[Type]): List[Type] = tp.dealias match case AndType(tp1, tp2) => - def decomposeComponent(tpA: Type, tpB: Type): List[Type] = - rec(tpA, tpB :: mixins).collect { - case tp if tp <:< tpB => tp - case tp if tpB <:< tp => tpB - case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) - } - - if canDecompose(tp1) then - decomposeComponent(tp1, tp2) - else - decomposeComponent(tp2, tp1) - - case OrType(tp1, tp2) => List(tp1, tp2) - case tp if tp.isRef(defn.BooleanClass) => - List( - ConstantType(Constant(true)), - ConstantType(Constant(false)) - ) - case tp if tp.isRef(defn.UnitClass) => - ConstantType(Constant(())) :: Nil - case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => - tp.classSymbol.children.map(_.termRef) - - case tp @ AppliedType(tycon, targs) if tp.classSymbol.children.isEmpty && canDecompose(tycon) => + var tpB = tp2 + var parts = rec(tp1, tp2 :: mixins) + if parts == ListOfNoType then + tpB = tp1 + parts = rec(tp2, tp1 :: mixins) + if parts == ListOfNoType then ListOfNoType + else parts.collect: + case tp if tp <:< tpB => tp + case tp if tpB <:< tp => tpB + case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) + + case OrType(tp1, tp2) => List(tp1, tp2) + case _: SingletonType => ListOfNoType + case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) + case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil + case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) + case tp: NamedType if canDec(tp.prefix) => dec(tp.prefix).map(tp.derivedSelect) + + case tp @ AppliedType(tycon, targs) if tp.classSymbol.children.isEmpty && canDec(tycon) => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. // But this is guarded by `tp.classSymbol.children.isEmpty`, // meaning we'll decompose to the same class, just not the same type. // For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`. - rec(tycon, Nil).map(tp.derivedAppliedType(_, targs)) - - case tp: NamedType if canDecompose(tp.prefix) => - rec(tp.prefix, Nil).map(tp.derivedSelect) + dec(tycon).map(tp.derivedAppliedType(_, targs)) - case tp => + case tp if { + val cls = tp.classSymbol + cls.is(Sealed) + && cls.isOneOf(AbstractOrTrait) + && !cls.hasAnonymousChild + && cls.children.nonEmpty + } => def getChildren(sym: Symbol): List[Symbol] = sym.children.flatMap { child => if child eq sym then List(sym) // i3145: sealed trait Baz, val x = new Baz {}, Baz.children returns Baz... @@ -646,33 +653,17 @@ object SpaceEngine { if inhabited(refined) then refined else NoType }.filter(_.exists) - debug.println(i"$tp decomposes to $parts") - parts - } - rec(tp, Nil).map(Typ(_, decomposed = true)) + + case _ => ListOfNoType + end rec + + rec(tp, Nil) } - /** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */ - def canDecompose(tp: Type)(using Context): Boolean = - val res = tp.dealias match - case AppliedType(tycon, _) if canDecompose(tycon) => true - case tp: NamedType if canDecompose(tp.prefix) => true - case _: SingletonType => false - case _: OrType => true - case AndType(tp1, tp2) => canDecompose(tp1) || canDecompose(tp2) - case _ => - val cls = tp.classSymbol - cls.is(Sealed) - && cls.isOneOf(AbstractOrTrait) - && !cls.hasAnonymousChild - && cls.children.nonEmpty - || cls.isAllOf(JavaEnumTrait) - || tp.isRef(defn.BooleanClass) - || tp.isRef(defn.UnitClass) - //debug.println(s"decomposable: ${tp.show} = $res") - res + val ListOfNoType = List(NoType) + val ListOfTypNoType = ListOfNoType.map(Typ(_, decomposed = true)) /** Show friendly type name with current scope in mind * From 917b6ea7658f4b8e1536b3f6c3d4fcd2b6a326c9 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 16 Feb 2023 15:08:38 +0000 Subject: [PATCH 08/16] Space: Use a custom Parts extractor --- .../tools/dotc/transform/patmat/Space.scala | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index cffe6a378649..64e0d9a94a1a 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -83,7 +83,9 @@ case class Typ(tp: Type, decomposed: Boolean = true) extends Space: def canDecompose(using Context): Boolean = decompose != SpaceEngine.ListOfTypNoType def decompose(using Context): List[Typ] = - if myDecompose == null then myDecompose = SpaceEngine.decompose(tp).map(Typ(_, decomposed = true)) + if myDecompose == null then myDecompose = tp match + case SpaceEngine.Parts(parts) => parts.map(Typ(_, decomposed = true)) + case _ => SpaceEngine.ListOfTypNoType myDecompose end Typ @@ -581,18 +583,6 @@ object SpaceEngine { /** Decompose a type into subspaces -- assume the type can be decomposed */ def decompose(tp: Type)(using Context): List[Type] = trace(i"decompose($tp)", debug) { - var lastType: Type = NoType - var lastParts: List[Type] = Nil - def dec(tp: Type) = - if tp eq lastType then lastParts - else - lastType = tp - lastParts = decompose(tp) - lastParts - def canDec(tp: Type) = - lastParts = dec(tp) - lastParts != ListOfNoType - def rec(tp: Type, mixins: List[Type]): List[Type] = tp.dealias match case AndType(tp1, tp2) => var tpB = tp2 @@ -611,14 +601,14 @@ object SpaceEngine { case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) - case tp: NamedType if canDec(tp.prefix) => dec(tp.prefix).map(tp.derivedSelect) + case tp @ TypeRef(Parts(parts), _) => parts.map(tp.derivedSelect) - case tp @ AppliedType(tycon, targs) if tp.classSymbol.children.isEmpty && canDec(tycon) => + case tp @ AppliedType(Parts(parts), targs) if tp.classSymbol.children.isEmpty => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. // But this is guarded by `tp.classSymbol.children.isEmpty`, // meaning we'll decompose to the same class, just not the same type. // For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`. - dec(tycon).map(tp.derivedAppliedType(_, targs)) + parts.map(tp.derivedAppliedType(_, targs)) case tp if { val cls = tp.classSymbol @@ -665,6 +655,12 @@ object SpaceEngine { val ListOfNoType = List(NoType) val ListOfTypNoType = ListOfNoType.map(Typ(_, decomposed = true)) + object Parts: + def unapply(tp: Type)(using Context): PartsExtractor = PartsExtractor(decompose(tp)) + + final class PartsExtractor(val get: List[Type]) extends AnyVal: + def isEmpty: Boolean = get == ListOfNoType + /** Show friendly type name with current scope in mind * * E.g. C.this.B --> B if current owner is C From e0ccff0484187a5fcbfd21cc850294bb83f0fb40 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Sun, 15 Jan 2023 17:57:34 +0000 Subject: [PATCH 09/16] Space: Cache simplify --- .../tools/dotc/transform/patmat/Space.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 64e0d9a94a1a..b5cc9a034812 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -66,6 +66,12 @@ sealed trait Space: else trace(s"isSubspace(${show(this)}, ${show(b)})", debug) { isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(this, b)) } + + private var mySimplified: Space = _ + + def simplify(using Context): Space = + if mySimplified == null then mySimplified = SpaceEngine.computeSimplify(this) + mySimplified end Space /** Empty space */ @@ -98,8 +104,13 @@ case class Or(spaces: Seq[Space]) extends Space object SpaceEngine { import tpd._ + def simplify(space: Space)(using Context): Space = space.simplify + def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b) + def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose + def decompose(typ: Typ)(using Context): List[Typ] = typ.decompose + /** Simplify space such that a space equal to `Empty` becomes `Empty` */ - def simplify(space: Space)(using Context): Space = trace(s"simplify ${show(space)} --> ", debug, show)(space match { + def computeSimplify(space: Space)(using Context): Space = trace(s"simplify ${show(space)} --> ", debug, show)(space match { case Prod(tp, fun, spaces) => val sps = spaces.mapconserve(simplify) if sps.contains(Empty) then Empty @@ -150,8 +161,6 @@ object SpaceEngine { } /** Is `a` a subspace of `b`? Equivalent to `simplify(simplify(a) - simplify(b)) == Empty`, but faster */ - def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b) - def computeIsSubspace(a: Space, b: Space)(using Context): Boolean = { val a2 = simplify(a) val b2 = simplify(b) @@ -578,9 +587,6 @@ object SpaceEngine { scrutineeTp <:< tp } - def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose - def decompose(typ: Typ)(using Context): List[Typ] = typ.decompose - /** Decompose a type into subspaces -- assume the type can be decomposed */ def decompose(tp: Type)(using Context): List[Type] = trace(i"decompose($tp)", debug) { def rec(tp: Type, mixins: List[Type]): List[Type] = tp.dealias match From d58805d2d4999249f552eaa10e70a45f5b3e0136 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Sun, 19 Feb 2023 16:00:16 +0000 Subject: [PATCH 10/16] Space: make code pass -Yexplicit-nulls --- .../tools/dotc/transform/patmat/Space.scala | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index b5cc9a034812..3fd3064d2168 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -67,11 +67,15 @@ sealed trait Space: isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(this, b)) } - private var mySimplified: Space = _ + private var mySimplified: Space | Null = _ def simplify(using Context): Space = - if mySimplified == null then mySimplified = SpaceEngine.computeSimplify(this) - mySimplified + val simplified = mySimplified + if simplified == null then + val simplified = SpaceEngine.computeSimplify(this) + mySimplified = simplified + simplified + else simplified end Space /** Empty space */ @@ -84,15 +88,19 @@ case object Empty extends Space * */ case class Typ(tp: Type, decomposed: Boolean = true) extends Space: - private var myDecompose: List[Typ] = _ + private var myDecompose: List[Typ] | Null = _ def canDecompose(using Context): Boolean = decompose != SpaceEngine.ListOfTypNoType def decompose(using Context): List[Typ] = - if myDecompose == null then myDecompose = tp match - case SpaceEngine.Parts(parts) => parts.map(Typ(_, decomposed = true)) - case _ => SpaceEngine.ListOfTypNoType - myDecompose + val decompose = myDecompose + if decompose == null then + val decompose = tp match + case SpaceEngine.Parts(parts) => parts.map(Typ(_, decomposed = true)) + case _ => SpaceEngine.ListOfTypNoType + myDecompose = decompose + decompose + else decompose end Typ /** Space representing an extractor pattern */ From 2a4aef069461e25da1821605f9ddd59df470ce72 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 20 Feb 2023 10:28:41 +0000 Subject: [PATCH 11/16] Space: Fix decomposable prefixes of TermRefs --- compiler/src/dotty/tools/dotc/core/Types.scala | 5 ++++- compiler/src/dotty/tools/dotc/transform/patmat/Space.scala | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d2fc225ff19d..e8cbced423bc 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2182,7 +2182,7 @@ object Types { // --- NamedTypes ------------------------------------------------------------------ - abstract class NamedType extends CachedProxyType, ValueType { self => + abstract class NamedType extends CachedProxyType, ValueType, Product { self => type ThisType >: this.type <: NamedType type ThisName <: Name @@ -2190,6 +2190,8 @@ object Types { val prefix: Type def designator: Designator protected def designator_=(d: Designator): Unit + def _1: Type + def _2: Designator assert(NamedType.validPrefix(prefix), s"invalid prefix $prefix") @@ -2905,6 +2907,7 @@ object Types { def apply(prefix: Type, designator: Name, denot: Denotation)(using Context): NamedType = if (designator.isTermName) TermRef.apply(prefix, designator.asTermName, denot) else TypeRef.apply(prefix, designator.asTypeName, denot) + def unapply(tp: NamedType): NamedType = tp def validPrefix(prefix: Type): Boolean = prefix.isValueType || (prefix eq NoPrefix) } diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 3fd3064d2168..859528e72e89 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -611,11 +611,11 @@ object SpaceEngine { case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) case OrType(tp1, tp2) => List(tp1, tp2) - case _: SingletonType => ListOfNoType case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) - case tp @ TypeRef(Parts(parts), _) => parts.map(tp.derivedSelect) + case tp @ NamedType(Parts(parts), _) => parts.map(tp.derivedSelect) + case _: SingletonType => ListOfNoType case tp @ AppliedType(Parts(parts), targs) if tp.classSymbol.children.isEmpty => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. From fbead6cc5e1da619eac968951aadfb6366c6fd10 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 20 Feb 2023 14:22:39 +0000 Subject: [PATCH 12/16] Space: Fix decomposable, dont recurse on Java enums --- .../src/dotty/tools/dotc/transform/patmat/Space.scala | 3 ++- tests/patmat/java-enum1/ParameterModifier.java | 8 ++++++++ tests/patmat/java-enum1/Test.scala | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/patmat/java-enum1/ParameterModifier.java create mode 100644 tests/patmat/java-enum1/Test.scala diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 859528e72e89..93800df6eeb8 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -613,9 +613,10 @@ object SpaceEngine { case OrType(tp1, tp2) => List(tp1, tp2) case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil - case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) case tp @ NamedType(Parts(parts), _) => parts.map(tp.derivedSelect) case _: SingletonType => ListOfNoType + case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef) + // the class of a java enum value is the enum class, so this must follow SingletonType to not loop infinitely case tp @ AppliedType(Parts(parts), targs) if tp.classSymbol.children.isEmpty => // It might not obvious that it's OK to apply the type arguments of a parent type to child types. diff --git a/tests/patmat/java-enum1/ParameterModifier.java b/tests/patmat/java-enum1/ParameterModifier.java new file mode 100644 index 000000000000..c9ddc157ba7e --- /dev/null +++ b/tests/patmat/java-enum1/ParameterModifier.java @@ -0,0 +1,8 @@ +public enum ParameterModifier { + Repeated, + Plain, + ByName; + + private ParameterModifier() { + } +} diff --git a/tests/patmat/java-enum1/Test.scala b/tests/patmat/java-enum1/Test.scala new file mode 100644 index 000000000000..b6ea483d8fb4 --- /dev/null +++ b/tests/patmat/java-enum1/Test.scala @@ -0,0 +1,6 @@ +class Test: + private def showParameterModifier(base: String, pm: ParameterModifier): String = pm match { + case ParameterModifier.Plain => base + case ParameterModifier.Repeated => base + "*" + case ParameterModifier.ByName => "=> " + base + } From 24cc7236edad681d94c951add27554b33219ae53 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 20 Feb 2023 21:16:21 +0000 Subject: [PATCH 13/16] Space: Fix isSubspace invariant by using simplify --- .../src/dotty/tools/dotc/transform/patmat/Space.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 93800df6eeb8..0c1ead0e71cb 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -61,10 +61,14 @@ sealed trait Space: private val isSubspaceCache = mutable.HashMap.empty[Space, Boolean] def isSubspace(b: Space)(using Context): Boolean = - if this == Empty then true + val a = this + val a2 = a.simplify + val b2 = b.simplify + if (a ne a2) || (b ne b2) then a2.isSubspace(b2) + else if a == Empty then true else if b == Empty then false else trace(s"isSubspace(${show(this)}, ${show(b)})", debug) { - isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(this, b)) + isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(a, b)) } private var mySimplified: Space | Null = _ From 1f57accfa3af2b620ec0e39b589afeebe8ebbe3b Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 21 Feb 2023 09:19:15 +0000 Subject: [PATCH 14/16] Space: Make new mutation `@sharable` --- compiler/src/dotty/tools/dotc/transform/patmat/Space.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 0c1ead0e71cb..87c8d3760067 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -23,6 +23,7 @@ import reporting._ import config.Printers.{exhaustivity => debug} import util.{SrcPos, NoSourcePosition} +import scala.annotation.internal.sharable import scala.collection.mutable /* Space logic for checking exhaustivity and unreachability of pattern matching @@ -58,7 +59,7 @@ import scala.collection.mutable sealed trait Space: import SpaceEngine.* - private val isSubspaceCache = mutable.HashMap.empty[Space, Boolean] + @sharable private val isSubspaceCache = mutable.HashMap.empty[Space, Boolean] def isSubspace(b: Space)(using Context): Boolean = val a = this @@ -71,7 +72,7 @@ sealed trait Space: isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(a, b)) } - private var mySimplified: Space | Null = _ + @sharable private var mySimplified: Space | Null = _ def simplify(using Context): Space = val simplified = mySimplified From a5858985e8170b5fede85217a9f7d607dec160cc Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 22 Feb 2023 11:31:52 +0000 Subject: [PATCH 15/16] Space: Extract & document isDecomposableToChildren --- .../tools/dotc/transform/patmat/Space.scala | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 87c8d3760067..b33719063a3f 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -630,13 +630,7 @@ object SpaceEngine { // For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`. parts.map(tp.derivedAppliedType(_, targs)) - case tp if { - val cls = tp.classSymbol - cls.is(Sealed) - && cls.isOneOf(AbstractOrTrait) - && !cls.hasAnonymousChild - && cls.children.nonEmpty - } => + case tp if tp.classSymbol.isDecomposableToChildren => def getChildren(sym: Symbol): List[Symbol] = sym.children.flatMap { child => if child eq sym then List(sym) // i3145: sealed trait Baz, val x = new Baz {}, Baz.children returns Baz... @@ -672,6 +666,18 @@ object SpaceEngine { rec(tp, Nil) } + extension (cls: Symbol) + /** A type is decomposable to children if it's sealed, + * abstract (or a trait) - so its not a sealed concrete class that can be instantiated on its own, + * has no anonymous children, which we wouldn't be able to name as counter-examples, + * but does have children. + * + * A sealed trait with no subclasses is considered not decomposable and thus is treated as an opaque type. + * A sealed trait with subclasses that then get removed after `refineUsingParent`, decomposes to the empty list. + * So that's why we consider whether a type has children. */ + def isDecomposableToChildren(using Context): Boolean = + cls.is(Sealed) && cls.isOneOf(AbstractOrTrait) && !cls.hasAnonymousChild && cls.children.nonEmpty + val ListOfNoType = List(NoType) val ListOfTypNoType = ListOfNoType.map(Typ(_, decomposed = true)) From daef636b9431e4cdd3439489c6b2f1890e89dcaf Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 22 Feb 2023 11:36:24 +0000 Subject: [PATCH 16/16] Space: Initialise caches with null --- compiler/src/dotty/tools/dotc/transform/patmat/Space.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index b33719063a3f..d69371520413 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -72,7 +72,7 @@ sealed trait Space: isSubspaceCache.getOrElseUpdate(b, computeIsSubspace(a, b)) } - @sharable private var mySimplified: Space | Null = _ + @sharable private var mySimplified: Space | Null = null def simplify(using Context): Space = val simplified = mySimplified @@ -93,7 +93,7 @@ case object Empty extends Space * */ case class Typ(tp: Type, decomposed: Boolean = true) extends Space: - private var myDecompose: List[Typ] | Null = _ + private var myDecompose: List[Typ] | Null = null def canDecompose(using Context): Boolean = decompose != SpaceEngine.ListOfTypNoType