Skip to content

Fix #3015: use type inference to type child classes #3054

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 73 additions & 31 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import Types._
import Contexts._
import Flags._
import ast.Trees._
import ast.tpd
import ast.{tpd, untpd}
import Decorators._
import Symbols._
import StdNames._
import NameOps._
import Constants._
import typer.Applications._
import typer._
import Applications._
import Inferencing._
import ProtoTypes._
import transform.SymUtils._
import reporting.diagnostic.messages._
import config.Printers.{ exhaustivity => debug }
import config.Printers.{exhaustivity => debug}

/** Space logic for checking exhaustivity and unreachability of pattern matching
*
Expand Down Expand Up @@ -524,43 +527,80 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
case tp =>
val parts = children.map { sym =>
if (sym.is(ModuleClass))
refine(tp, sym.sourceModule.termRef)
else if (sym.isTerm)
refine(tp, sym.termRef)
else if (sym.info.typeParams.length > 0 || tp.isInstanceOf[TypeRef])
refine(tp, sym.typeRef)
refine(tp, sym.sourceModule)
else
sym.typeRef
} filter { tpe =>
// Child class may not always be subtype of parent:
// GADT & path-dependent types
val res = tpe <:< expose(tp)
if (!res) debug.println(s"unqualified child ousted: ${tpe.show} !< ${tp.show}")
res
}
refine(tp, sym)
} filter(_.exists)

debug.println(s"${tp.show} decomposes to [${parts.map(_.show).mkString(", ")}]")

parts.map(Typ(_, true))
}
}

/** Refine tp2 based on tp1
/** Refine child based on parent
*
* In child class definition, we have:
*
* class Child[Ts] extends path.Parent[Us] with Es
* object Child extends path.Parent[Us] with Es
* val child = new path.Parent[Us] with Es // enum values
*
* Given a parent type `parent` and a child symbol `child`, we infer the prefix
* and type parameters for the child:
*
* prefix.child[Vs] <:< parent
*
* where `Vs` are fresh type variables and `prefix` is the symbol prefix with all
* non-module and non-package `ThisType` replaced by fresh type variables.
*
* If the subtyping is true, the instantiated type `p.child[Vs]` is
* returned. Otherwise, `NoType` is returned.
*
*/
def refine(parent: Type, child: Symbol): Type = {
if (child.isTerm && child.is(Case, butNot = Module)) return child.termRef // enum vals always match

val childTp = if (child.isTerm) child.termRef else child.typeRef

val resTp = instantiate(childTp, expose(parent))(ctx.fresh.setNewTyperState)

if (!resTp.exists) {
debug.println(s"[refine] unqualified child ousted: ${childTp.show} !< ${parent.show}")
NoType
}
else {
debug.println(s"$child instantiated ------> $resTp")
resTp
}
}

/** Instantiate type `tp1` to be a subtype of `tp2`
*
* Return the instantiated type if type parameters and this type
* in `tp1` can be instantiated such that `tp1 <:< tp2`.
*
* E.g. if `tp1` is `Option[Int]`, `tp2` is `Some`, then return
* `Some[Int]`.
* Otherwise, return NoType.
*
* If `tp1` is `path1.A`, `tp2` is `path2.B`, and `path1` is subtype of
* `path2`, then return `path1.B`.
*/
def refine(tp1: Type, tp2: Type): Type = (tp1, tp2) match {
case (tp1: RefinedType, _: TypeRef) => tp1.wrapIfMember(refine(tp1.parent, tp2))
case (tp1: HKApply, _) => refine(tp1.superType, tp2)
case (TypeRef(ref1: TypeProxy, _), tp2 @ TypeRef(ref2: TypeProxy, _)) =>
if (ref1.underlying <:< ref2.underlying) tp2.derivedSelect(ref1) else tp2
case (TypeRef(ref1: TypeProxy, _), tp2 @ TermRef(ref2: TypeProxy, _)) =>
if (ref1.underlying <:< ref2.underlying) tp2.derivedSelect(ref1) else tp2
case _ => tp2
def instantiate(tp1: Type, tp2: Type)(implicit ctx: Context): Type = {
val approx = new TypeMap {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does approx do? A more telling name or a comment would help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, a better name is more readable.

def apply(t: Type): Type = t match {
case t @ ThisType(tref) if !tref.symbol.isStaticOwner && !tref.symbol.is(Module) =>
newTypeVar(TypeBounds.upper(mapOver(tref & tref.givenSelfType)))
Copy link
Contributor

@odersky odersky Sep 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want

newTypeVar(TypeBounds.upper(mapOver(t.underlying)))

here. That achieves what was written in simpler form. But why upper? Without an explanation it's hard to know whether it is correct. But it feels dangerous because the ThisType might appear at negative variance positions as well. How do you argue that the bound should not be changed in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument here is that the child type created from childSym.typeRef can only be of the shape path.Child, thus the ThisType is always covariant.

case _ =>
mapOver(t)
}
}

val tvars = tp1.typeParams.map { tparam => newTypeVar(tparam.paramInfo.bounds) }
val protoTp1 = approx(tp1.appliedTo(tvars))

if (protoTp1 <:< tp2 && isFullyDefined(protoTp1, ForceDegree.all)) protoTp1
else {
debug.println(s"$protoTp1 <:< $tp2 = false")
NoType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat that this works! We might want to move it to something more generally available since it looks like the Linker will need something like this was well /cc @nicolasstucki. @DarkDimius

}
}

/** Abstract sealed types, or-types, Boolean and Java enums can be decomposed */
Expand Down Expand Up @@ -590,20 +630,22 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
*
*/
def showType(tp: Type): String = {
val enclosingCls = ctx.owner.enclosingClass.asClass.classInfo.symbolicTypeRef
val enclosingCls = ctx.owner.enclosingClass

def isOmittable(sym: Symbol) =
sym.isEffectiveRoot || sym.isAnonymousClass || sym.name.isReplWrapperName ||
ctx.definitions.UnqualifiedOwnerTypes.exists(_.symbol == sym) ||
sym.showFullName.startsWith("scala.") ||
sym == enclosingCls.typeSymbol
sym == enclosingCls || sym == enclosingCls.sourceModule

def refinePrefix(tp: Type): String = tp match {
case NoPrefix => ""
case tp: NamedType if isOmittable(tp.symbol) => ""
case tp: ThisType => refinePrefix(tp.tref)
case tp: RefinedType => refinePrefix(tp.parent)
case tp: NamedType => tp.name.show.stripSuffix("$")
case tp: TypeVar => refinePrefix(tp.instanceOpt)
case _ => tp.show
}

def refine(tp: Type): String = tp match {
Expand Down