Skip to content

Commit 51d8cab

Browse files
authored
Merge pull request #5996 from dotty-staging/match-types
Match Types: implement cantPossiblyMatch
2 parents 8d09c11 + ea04343 commit 51d8cab

File tree

12 files changed

+609
-382
lines changed

12 files changed

+609
-382
lines changed

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 215 additions & 60 deletions
Large diffs are not rendered by default.

compiler/src/dotty/tools/dotc/core/TypeOps.scala

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import collection.mutable
1313
import ast.tpd._
1414
import reporting.trace
1515
import reporting.diagnostic.Message
16+
import config.Printers.{gadts, typr}
17+
import typer.Applications._
18+
import typer.ProtoTypes._
19+
import typer.ForceDegree
20+
import typer.Inferencing.isFullyDefined
1621

1722
import scala.annotation.internal.sharable
1823

@@ -334,6 +339,154 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
334339
* This test is used when we are too early in the pipeline to consider imports.
335340
*/
336341
def scala2Setting = ctx.settings.language.value.contains(nme.Scala2.toString)
342+
343+
/** Refine child based on parent
344+
*
345+
* In child class definition, we have:
346+
*
347+
* class Child[Ts] extends path.Parent[Us] with Es
348+
* object Child extends path.Parent[Us] with Es
349+
* val child = new path.Parent[Us] with Es // enum values
350+
*
351+
* Given a parent type `parent` and a child symbol `child`, we infer the prefix
352+
* and type parameters for the child:
353+
*
354+
* prefix.child[Vs] <:< parent
355+
*
356+
* where `Vs` are fresh type variables and `prefix` is the symbol prefix with all
357+
* non-module and non-package `ThisType` replaced by fresh type variables.
358+
*
359+
* If the subtyping is true, the instantiated type `p.child[Vs]` is
360+
* returned. Otherwise, `NoType` is returned.
361+
*/
362+
def refineUsingParent(parent: Type, child: Symbol)(implicit ctx: Context): Type = {
363+
if (child.isTerm && child.is(Case, butNot = Module)) return child.termRef // enum vals always match
364+
365+
// <local child> is a place holder from Scalac, it is hopeless to instantiate it.
366+
//
367+
// Quote from scalac (from nsc/symtab/classfile/Pickler.scala):
368+
//
369+
// ...When a sealed class/trait has local subclasses, a single
370+
// <local child> class symbol is added as pickled child
371+
// (instead of a reference to the anonymous class; that was done
372+
// initially, but seems not to work, ...).
373+
//
374+
if (child.name == tpnme.LOCAL_CHILD) return child.typeRef
375+
376+
val childTp = if (child.isTerm) child.termRef else child.typeRef
377+
378+
instantiate(childTp, parent)(ctx.fresh.setNewTyperState()).dealias
379+
}
380+
381+
/** Instantiate type `tp1` to be a subtype of `tp2`
382+
*
383+
* Return the instantiated type if type parameters and this type
384+
* in `tp1` can be instantiated such that `tp1 <:< tp2`.
385+
*
386+
* Otherwise, return NoType.
387+
*/
388+
private def instantiate(tp1: NamedType, tp2: Type)(implicit ctx: Context): Type = {
389+
/** expose abstract type references to their bounds or tvars according to variance */
390+
class AbstractTypeMap(maximize: Boolean)(implicit ctx: Context) extends TypeMap {
391+
def expose(lo: Type, hi: Type): Type =
392+
if (variance == 0)
393+
newTypeVar(TypeBounds(lo, hi))
394+
else if (variance == 1)
395+
if (maximize) hi else lo
396+
else
397+
if (maximize) lo else hi
398+
399+
def apply(tp: Type): Type = tp match {
400+
case tp: TypeRef if isBounds(tp.underlying) =>
401+
val lo = this(tp.info.loBound)
402+
val hi = this(tp.info.hiBound)
403+
// See tests/patmat/gadt.scala tests/patmat/exhausting.scala tests/patmat/t9657.scala
404+
val exposed = expose(lo, hi)
405+
typr.println(s"$tp exposed to =====> $exposed")
406+
exposed
407+
408+
case AppliedType(tycon: TypeRef, args) if isBounds(tycon.underlying) =>
409+
val args2 = args.map(this)
410+
val lo = this(tycon.info.loBound).applyIfParameterized(args2)
411+
val hi = this(tycon.info.hiBound).applyIfParameterized(args2)
412+
val exposed = expose(lo, hi)
413+
typr.println(s"$tp exposed to =====> $exposed")
414+
exposed
415+
416+
case _ =>
417+
mapOver(tp)
418+
}
419+
}
420+
421+
def minTypeMap(implicit ctx: Context) = new AbstractTypeMap(maximize = false)
422+
def maxTypeMap(implicit ctx: Context) = new AbstractTypeMap(maximize = true)
423+
424+
// Fix subtype checking for child instantiation,
425+
// such that `Foo(Test.this.foo) <:< Foo(Foo.this)`
426+
// See tests/patmat/i3938.scala
427+
class RemoveThisMap extends TypeMap {
428+
var prefixTVar: Type = null
429+
def apply(tp: Type): Type = tp match {
430+
case ThisType(tref: TypeRef) if !tref.symbol.isStaticOwner =>
431+
if (tref.symbol.is(Module))
432+
TermRef(this(tref.prefix), tref.symbol.sourceModule)
433+
else if (prefixTVar != null)
434+
this(tref)
435+
else {
436+
prefixTVar = WildcardType // prevent recursive call from assigning it
437+
prefixTVar = newTypeVar(TypeBounds.upper(this(tref)))
438+
prefixTVar
439+
}
440+
case tp => mapOver(tp)
441+
}
442+
}
443+
444+
// replace uninstantiated type vars with WildcardType, check tests/patmat/3333.scala
445+
def instUndetMap(implicit ctx: Context) = new TypeMap {
446+
def apply(t: Type): Type = t match {
447+
case tvar: TypeVar if !tvar.isInstantiated => WildcardType(tvar.origin.underlying.bounds)
448+
case _ => mapOver(t)
449+
}
450+
}
451+
452+
val removeThisType = new RemoveThisMap
453+
val tvars = tp1.typeParams.map { tparam => newTypeVar(tparam.paramInfo.bounds) }
454+
val protoTp1 = removeThisType.apply(tp1).appliedTo(tvars)
455+
456+
val force = new ForceDegree.Value(
457+
tvar =>
458+
!(ctx.typerState.constraint.entry(tvar.origin) `eq` tvar.origin.underlying) ||
459+
(tvar `eq` removeThisType.prefixTVar),
460+
minimizeAll = false,
461+
allowBottom = false
462+
)
463+
464+
// If parent contains a reference to an abstract type, then we should
465+
// refine subtype checking to eliminate abstract types according to
466+
// variance. As this logic is only needed in exhaustivity check,
467+
// we manually patch subtyping check instead of changing TypeComparer.
468+
// See tests/patmat/i3645b.scala
469+
def parentQualify = tp1.widen.classSymbol.info.parents.exists { parent =>
470+
implicit val ictx = ctx.fresh.setNewTyperState()
471+
parent.argInfos.nonEmpty && minTypeMap.apply(parent) <:< maxTypeMap.apply(tp2)
472+
}
473+
474+
if (protoTp1 <:< tp2) {
475+
if (isFullyDefined(protoTp1, force)) protoTp1
476+
else instUndetMap.apply(protoTp1)
477+
}
478+
else {
479+
val protoTp2 = maxTypeMap.apply(tp2)
480+
if (protoTp1 <:< protoTp2 || parentQualify) {
481+
if (isFullyDefined(AndType(protoTp1, protoTp2), force)) protoTp1
482+
else instUndetMap.apply(protoTp1)
483+
}
484+
else {
485+
typr.println(s"$protoTp1 <:< $protoTp2 = false")
486+
NoType
487+
}
488+
}
489+
}
337490
}
338491

339492
object TypeOps {

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,7 +2418,7 @@ object Types {
24182418
}
24192419
}
24202420

2421-
/** A constant type with single `value`. */
2421+
/** A constant type with single `value`. */
24222422
abstract case class ConstantType(value: Constant) extends CachedProxyType with SingletonType {
24232423
override def underlying(implicit ctx: Context): Type = value.tpe
24242424

@@ -3767,42 +3767,9 @@ object Types {
37673767

37683768
override def tryNormalize(implicit ctx: Context): Type = reduced.normalized
37693769

3770-
/** Switch to choose parallel or sequential reduction */
3771-
private final val reduceInParallel = false
3772-
3773-
final def cantPossiblyMatch(cas: Type)(implicit ctx: Context): Boolean =
3774-
true // should be refined if we allow overlapping cases
3775-
37763770
def reduced(implicit ctx: Context): Type = {
37773771
val trackingCtx = ctx.fresh.setTypeComparerFn(new TrackingTypeComparer(_))
3778-
val cmp = trackingCtx.typeComparer.asInstanceOf[TrackingTypeComparer]
3779-
3780-
def reduceSequential(cases: List[Type])(implicit ctx: Context): Type = cases match {
3781-
case Nil => NoType
3782-
case cas :: cases1 =>
3783-
val r = cmp.matchCase(scrutinee, cas, instantiate = true)
3784-
if (r.exists) r
3785-
else if (cantPossiblyMatch(cas)) reduceSequential(cases1)
3786-
else NoType
3787-
}
3788-
3789-
def reduceParallel(implicit ctx: Context) = {
3790-
val applicableBranches = cases
3791-
.map(cmp.matchCase(scrutinee, _, instantiate = true)(trackingCtx))
3792-
.filter(_.exists)
3793-
applicableBranches match {
3794-
case Nil => NoType
3795-
case applicableBranch :: Nil => applicableBranch
3796-
case _ =>
3797-
record(i"MatchType.multi-branch")
3798-
ctx.typeComparer.glb(applicableBranches)
3799-
}
3800-
}
3801-
3802-
def isBounded(tp: Type) = tp match {
3803-
case tp: TypeParamRef =>
3804-
case tp: TypeRef => ctx.gadt.contains(tp.symbol)
3805-
}
3772+
val typeComparer = trackingCtx.typeComparer.asInstanceOf[TrackingTypeComparer]
38063773

38073774
def contextInfo(tp: Type): Type = tp match {
38083775
case tp: TypeParamRef =>
@@ -3816,28 +3783,27 @@ object Types {
38163783
tp.underlying
38173784
}
38183785

3819-
def updateReductionContext() = {
3786+
def updateReductionContext(): Unit = {
38203787
reductionContext = new mutable.HashMap
3821-
for (tp <- cmp.footprint)
3788+
for (tp <- typeComparer.footprint)
38223789
reductionContext(tp) = contextInfo(tp)
3823-
typr.println(i"footprint for $this $hashCode: ${cmp.footprint.toList.map(x => (x, contextInfo(x)))}%, %")
3790+
typr.println(i"footprint for $this $hashCode: ${typeComparer.footprint.toList.map(x => (x, contextInfo(x)))}%, %")
38243791
}
38253792

3826-
def upToDate =
3793+
def isUpToDate: Boolean =
38273794
reductionContext.keysIterator.forall { tp =>
38283795
reductionContext(tp) `eq` contextInfo(tp)
38293796
}
38303797

38313798
record("MatchType.reduce called")
3832-
if (!Config.cacheMatchReduced || myReduced == null || !upToDate) {
3799+
if (!Config.cacheMatchReduced || myReduced == null || !isUpToDate) {
38333800
record("MatchType.reduce computed")
38343801
if (myReduced != null) record("MatchType.reduce cache miss")
38353802
myReduced =
38363803
trace(i"reduce match type $this $hashCode", typr, show = true) {
38373804
try
38383805
if (defn.isBottomType(scrutinee)) defn.NothingType
3839-
else if (reduceInParallel) reduceParallel(trackingCtx)
3840-
else reduceSequential(cases)(trackingCtx)
3806+
else typeComparer.matchCases(scrutinee, cases)(trackingCtx)
38413807
catch {
38423808
case ex: Throwable =>
38433809
handleRecursive("reduce type ", i"$scrutinee match ...", ex)

compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,20 @@ object TypeTestsCasts {
164164
else tp.classSymbol
165165

166166
def foundCls = effectiveClass(expr.tpe.widen)
167-
// println(i"ta $tree, found = $foundCls")
168167

169168
def inMatch =
170169
fun.symbol == defn.Any_typeTest || // new scheme
171170
expr.symbol.is(Case) // old scheme
172171

173-
def transformIsInstanceOf(expr:Tree, testType: Type, flagUnrelated: Boolean): Tree = {
172+
def transformIsInstanceOf(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = {
174173
def testCls = effectiveClass(testType.widen)
175174

176-
def unreachable(why: => String) =
175+
def unreachable(why: => String): Boolean = {
177176
if (flagUnrelated)
178177
if (inMatch) ctx.error(em"this case is unreachable since $why", expr.sourcePos)
179178
else ctx.warning(em"this will always yield false since $why", expr.sourcePos)
179+
false
180+
}
180181

181182
/** Are `foundCls` and `testCls` classes that allow checks
182183
* whether a test would be always false?
@@ -191,25 +192,22 @@ object TypeTestsCasts {
191192
// we don't have the logic to handle derived value classes
192193

193194
/** Check whether a runtime test that a value of `foundCls` can be a `testCls`
194-
* can be true in some cases. Issure a warning or an error if that's not the case.
195+
* can be true in some cases. Issues a warning or an error otherwise.
195196
*/
196197
def checkSensical: Boolean =
197198
if (!isCheckable) true
198199
else if (foundCls.isPrimitiveValueClass && !testCls.isPrimitiveValueClass) {
199-
ctx.error("cannot test if value types are references", tree.sourcePos)
200-
false
201-
}
200+
ctx.error("cannot test if value types are references", tree.sourcePos)
201+
false
202+
}
202203
else if (!foundCls.derivesFrom(testCls)) {
203-
if (foundCls.is(Final)) {
204+
val unrelated = !testCls.derivesFrom(foundCls) && (
205+
testCls.is(Final) || !testCls.is(Trait) && !foundCls.is(Trait)
206+
)
207+
if (foundCls.is(Final))
204208
unreachable(i"$foundCls is not a subclass of $testCls")
205-
false
206-
}
207-
else if (!testCls.derivesFrom(foundCls) &&
208-
(testCls.is(Final) ||
209-
!testCls.is(Trait) && !foundCls.is(Trait))) {
209+
else if (unrelated)
210210
unreachable(i"$foundCls and $testCls are unrelated")
211-
false
212-
}
213211
else true
214212
}
215213
else true

compiler/src/dotty/tools/dotc/transform/TypeUtils.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
package dotty.tools.dotc
1+
package dotty.tools
2+
package dotc
23
package transform
34

45
import core._

0 commit comments

Comments
 (0)