Skip to content

Commit 6182ad1

Browse files
committed
Use new specced match types for class type constructors.
This is the first step in using the new specified algorithm for match type reduction. When the pattern of a case satisfies elibility conditions, we use the new algorithm. Otherwise, we fall back on the legacy algorithm. To be eligible, a pattern with at least one capture must be: an applied *class* type constructor whose arguments are all: - either a type capture, - or a fully defined type that contains no inner capture, - or the argument must be in covariant position and recursively qualify to the elibility conditions. With those criteria, all the type captures can be *computed* using `baseType`, instead of inferred through the full `TypeComparer` machinery. The new algorithm directly handles preventing widening abstract types, when doing so leads to captures being under-defined. With the legacy algorithm, this prevention is scattered elsewhere in the type comparer. Making it centralized improves the error messages in those situations; it seems they were previously entirely misleading (see changed check files).
1 parent d44273b commit 6182ad1

File tree

8 files changed

+322
-17
lines changed

8 files changed

+322
-17
lines changed

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

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,11 +3218,22 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32183218
}
32193219
}
32203220

3221+
def instantiateParamsSpec(insts: Array[Type], caseLambda: HKTypeLambda) = new TypeMap {
3222+
variance = 0
3223+
3224+
def apply(t: Type) = t match {
3225+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
3226+
case t: LazyRef => apply(t.ref)
3227+
case _ => mapOver(t)
3228+
}
3229+
}
3230+
32213231
/** Match a single case. */
32223232
def matchCase(cas: MatchTypeCaseSpec): MatchResult = trace(i"$scrut match ${MatchTypeTrace.caseText(cas)}", matchTypes, show = true) {
32233233
cas match
3224-
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3225-
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
3234+
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3235+
case cas: MatchTypeCaseSpec.SpeccedPatMat => matchSpeccedPatMat(cas)
3236+
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
32263237
}
32273238

32283239
def matchSubTypeTest(spec: MatchTypeCaseSpec.SubTypeTest): MatchResult =
@@ -3234,6 +3245,128 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32343245
MatchResult.Stuck
32353246
end matchSubTypeTest
32363247

3248+
def matchSpeccedPatMat(spec: MatchTypeCaseSpec.SpeccedPatMat): MatchResult =
3249+
/* Concreteness checking
3250+
*
3251+
* When following a baseType and reaching a non-wildcard, in-variant-pos type capture,
3252+
* we have to make sure that the scrutinee is concrete enough to uniquely determine
3253+
* the values of the captures. This comes down to checking that we do not follow any
3254+
* upper bound of an abstract type.
3255+
*
3256+
* See notably neg/wildcard-match.scala for examples of this.
3257+
*/
3258+
3259+
def followEverythingConcrete(tp: Type): Type =
3260+
val widenedTp = tp.widenDealias
3261+
val tp1 = widenedTp.normalized
3262+
3263+
def followTp1: Type =
3264+
// If both widenDealias and normalized did something, start again
3265+
if (tp1 ne widenedTp) && (widenedTp ne tp) then followEverythingConcrete(tp1)
3266+
else tp1
3267+
3268+
tp1 match
3269+
case tp1: TypeRef =>
3270+
tp1.info match
3271+
case TypeAlias(tl: HKTypeLambda) => tl
3272+
case MatchAlias(tl: HKTypeLambda) => tl
3273+
case _ => followTp1
3274+
case tp1 @ AppliedType(tycon, args) =>
3275+
val concreteTycon = followEverythingConcrete(tycon)
3276+
if concreteTycon eq tycon then followTp1
3277+
else followEverythingConcrete(concreteTycon.applyIfParameterized(args))
3278+
case _ =>
3279+
followTp1
3280+
end followEverythingConcrete
3281+
3282+
def isConcrete(tp: Type): Boolean =
3283+
followEverythingConcrete(tp) match
3284+
case tp1: AndOrType => isConcrete(tp1.tp1) && isConcrete(tp1.tp2)
3285+
case tp1 => tp1.underlyingClassRef(refinementOK = true).exists
3286+
3287+
// Actuall matching logic
3288+
3289+
val instances = Array.fill[Type](spec.captureCount)(NoType)
3290+
3291+
def rec(pattern: MatchTypeCasePattern, scrut: Type, variance: Int, scrutIsWidenedAbstract: Boolean): Boolean =
3292+
pattern match
3293+
case MatchTypeCasePattern.Capture(num, isWildcard) =>
3294+
instances(num) = scrut match
3295+
case scrut: TypeBounds =>
3296+
if isWildcard then
3297+
// anything will do, as long as it conforms to the bounds for the subsequent `scrut <:< instantiatedPat` test
3298+
scrut.hi
3299+
else if scrutIsWidenedAbstract then
3300+
// always keep the TypeBounds so that we can report the correct NoInstances
3301+
scrut
3302+
else
3303+
variance match
3304+
case 1 => scrut.hi
3305+
case -1 => scrut.lo
3306+
case 0 => scrut
3307+
case _ =>
3308+
if !isWildcard && scrutIsWidenedAbstract && variance != 0 then
3309+
// force a TypeBounds to report the correct NoInstances
3310+
// the Nothing and Any bounds are used so that they are not displayed; not for themselves in particular
3311+
if variance > 0 then TypeBounds(defn.NothingType, scrut)
3312+
else TypeBounds(scrut, defn.AnyType)
3313+
else
3314+
scrut
3315+
!instances(num).isError
3316+
3317+
case MatchTypeCasePattern.TypeTest(tpe) =>
3318+
// The actual type test is handled by `scrut <:< instantiatedPat`
3319+
true
3320+
3321+
case MatchTypeCasePattern.BaseTypeTest(classType, argPatterns, needsConcreteScrut) =>
3322+
val cls = classType.classSymbol.asClass
3323+
scrut.baseType(cls) match
3324+
case base @ AppliedType(baseTycon, baseArgs) if baseTycon =:= classType =>
3325+
val innerScrutIsWidenedAbstract =
3326+
scrutIsWidenedAbstract
3327+
|| (needsConcreteScrut && !isConcrete(scrut)) // no point in checking concreteness if it does not need to be concrete
3328+
3329+
def matchArgs(argPatterns: List[MatchTypeCasePattern], baseArgs: List[Type], tparams: List[TypeParamInfo]): Boolean =
3330+
if argPatterns.isEmpty then
3331+
true
3332+
else
3333+
rec(argPatterns.head, baseArgs.head, tparams.head.paramVarianceSign, innerScrutIsWidenedAbstract)
3334+
&& matchArgs(argPatterns.tail, baseArgs.tail, tparams.tail)
3335+
3336+
matchArgs(argPatterns, baseArgs, classType.typeParams)
3337+
3338+
case _ =>
3339+
false
3340+
end rec
3341+
3342+
// This might not be needed
3343+
val contrainedCaseLambda = constrained(spec.origMatchCase).asInstanceOf[HKTypeLambda]
3344+
3345+
def tryDisjoint: MatchResult =
3346+
val defn.MatchCase(origPattern, _) = contrainedCaseLambda.resultType: @unchecked
3347+
if provablyDisjoint(scrut, origPattern) then
3348+
MatchResult.Disjoint
3349+
else
3350+
MatchResult.Stuck
3351+
3352+
if rec(spec.pattern, scrut, variance = 1, scrutIsWidenedAbstract = false) then
3353+
if instances.exists(_.isInstanceOf[TypeBounds]) then
3354+
MatchResult.NoInstance {
3355+
contrainedCaseLambda.paramNames.zip(instances).collect {
3356+
case (name, bounds: TypeBounds) => (name, bounds)
3357+
}
3358+
}
3359+
else
3360+
val defn.MatchCase(instantiatedPat, reduced) =
3361+
instantiateParamsSpec(instances, contrainedCaseLambda)(contrainedCaseLambda.resultType): @unchecked
3362+
if scrut <:< instantiatedPat then
3363+
MatchResult.Reduced(reduced)
3364+
else
3365+
tryDisjoint
3366+
else
3367+
tryDisjoint
3368+
end matchSpeccedPatMat
3369+
32373370
def matchLegacyPatMat(spec: MatchTypeCaseSpec.LegacyPatMat): MatchResult =
32383371
val caseLambda = constrained(spec.origMatchCase).asInstanceOf[HKTypeLambda]
32393372
this.caseLambda = caseLambda

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Flags._
77
import Names._
88
import StdNames._, NameOps._
99
import NullOpsDecorator._
10-
import NameKinds.SkolemName
10+
import NameKinds.{SkolemName, WildcardParamName}
1111
import Scopes._
1212
import Constants._
1313
import Contexts._
@@ -43,7 +43,7 @@ import scala.annotation.internal.sharable
4343
import scala.annotation.threadUnsafe
4444

4545
import dotty.tools.dotc.transform.SymUtils._
46-
import dotty.tools.dotc.transform.TypeUtils.isErasedClass
46+
import dotty.tools.dotc.transform.TypeUtils.{isErasedClass, toNestedPairs}
4747

4848
object Types {
4949

@@ -5055,8 +5055,23 @@ object Types {
50555055
case _ => None
50565056
}
50575057

5058+
enum MatchTypeCasePattern:
5059+
case Capture(num: Int, isWildcard: Boolean)
5060+
case TypeTest(tpe: Type)
5061+
case BaseTypeTest(classType: TypeRef, argPatterns: List[MatchTypeCasePattern], needsConcreteScrut: Boolean)
5062+
5063+
def isTypeTest: Boolean =
5064+
this.isInstanceOf[TypeTest]
5065+
5066+
def needsConcreteScrutInVariantPos: Boolean = this match
5067+
case Capture(_, isWildcard) => !isWildcard
5068+
case TypeTest(_) => false
5069+
case _ => true
5070+
end MatchTypeCasePattern
5071+
50585072
enum MatchTypeCaseSpec:
50595073
case SubTypeTest(origMatchCase: Type, pattern: Type, body: Type)
5074+
case SpeccedPatMat(origMatchCase: HKTypeLambda, captureCount: Int, pattern: MatchTypeCasePattern, body: Type)
50605075
case LegacyPatMat(origMatchCase: HKTypeLambda)
50615076

50625077
def origMatchCase: Type
@@ -5066,11 +5081,59 @@ object Types {
50665081
def analyze(cas: Type)(using Context): MatchTypeCaseSpec =
50675082
cas match
50685083
case cas: HKTypeLambda =>
5069-
LegacyPatMat(cas)
5084+
val defn.MatchCase(pat, body) = cas.resultType: @unchecked
5085+
val specPattern = tryConvertToSpecPattern(cas, pat)
5086+
if specPattern != null then
5087+
SpeccedPatMat(cas, cas.paramNames.size, specPattern, body)
5088+
else
5089+
LegacyPatMat(cas)
50705090
case _ =>
50715091
val defn.MatchCase(pat, body) = cas: @unchecked
50725092
SubTypeTest(cas, pat, body)
50735093
end analyze
5094+
5095+
private def tryConvertToSpecPattern(caseLambda: HKTypeLambda, pat: Type)(using Context): MatchTypeCasePattern | Null =
5096+
var typeParamRefsAccountedFor: Int = 0
5097+
5098+
def rec(pat: Type, variance: Int): MatchTypeCasePattern | Null =
5099+
pat match
5100+
case pat @ TypeParamRef(binder, num) if binder eq caseLambda =>
5101+
typeParamRefsAccountedFor += 1
5102+
MatchTypeCasePattern.Capture(num, isWildcard = pat.paramName.is(WildcardParamName))
5103+
5104+
case pat @ AppliedType(tycon: TypeRef, args) if variance == 1 =>
5105+
val tyconSym = tycon.symbol
5106+
if tyconSym.isClass then
5107+
val cls = tyconSym.asClass
5108+
if cls.name.startsWith("Tuple") && defn.isTupleNType(pat) then
5109+
rec(pat.toNestedPairs, variance)
5110+
else
5111+
val tparams = tycon.typeParams
5112+
val argPatterns = args.zip(tparams).map { (arg, tparam) =>
5113+
rec(arg, tparam.paramVarianceSign)
5114+
}
5115+
if argPatterns.exists(_ == null) then
5116+
null
5117+
else
5118+
val argPatterns1 = argPatterns.asInstanceOf[List[MatchTypeCasePattern]] // they are not null
5119+
if argPatterns1.forall(_.isTypeTest) then
5120+
MatchTypeCasePattern.TypeTest(pat)
5121+
else
5122+
val needsConcreteScrut = argPatterns1.zip(tparams).exists {
5123+
(argPattern, tparam) => tparam.paramVarianceSign != 0 && argPattern.needsConcreteScrutInVariantPos
5124+
}
5125+
MatchTypeCasePattern.BaseTypeTest(tycon, argPatterns1, needsConcreteScrut)
5126+
else
5127+
null
5128+
5129+
case _ =>
5130+
MatchTypeCasePattern.TypeTest(pat)
5131+
end rec
5132+
5133+
val result = rec(pat, variance = 1)
5134+
if typeParamRefsAccountedFor == caseLambda.paramNames.size then result
5135+
else null
5136+
end tryConvertToSpecPattern
50745137
end MatchTypeCaseSpec
50755138

50765139
// ------ ClassInfo, Type Bounds --------------------------------------------------

tests/neg/6570-1.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
| does not uniquely determine parameter x in
2828
| case Cov[x] => N[x]
2929
| The computed bounds for the parameter are:
30-
| x >: Box[Int]
30+
| x <: Box[Int]
3131
|
3232
| longer explanation available when compiling with `-explain`

tests/neg/i11982a.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
| does not uniquely determine parameter xs in
1111
| case _ *: xs => xs
1212
| The computed bounds for the parameter are:
13-
| xs >: Any *: EmptyTuple.type <: Tuple
13+
| xs <: Any *: EmptyTuple.type
1414
|
1515
| longer explanation available when compiling with `-explain`
1616
-- [E057] Type Mismatch Error: tests/neg/i11982a.scala:10:38 -----------------------------------------------------------
@@ -25,7 +25,7 @@
2525
| does not uniquely determine parameter xs in
2626
| case _ *: xs => xs
2727
| The computed bounds for the parameter are:
28-
| xs >: Any *: EmptyTuple.type <: Tuple
28+
| xs <: Any *: EmptyTuple.type
2929
|
3030
| longer explanation available when compiling with `-explain`
3131
-- [E057] Type Mismatch Error: tests/neg/i11982a.scala:12:25 -----------------------------------------------------------
@@ -40,6 +40,6 @@
4040
| does not uniquely determine parameter xs in
4141
| case _ *: xs => xs
4242
| The computed bounds for the parameter are:
43-
| xs >: Any *: EmptyTuple.type <: Tuple
43+
| xs <: Any *: EmptyTuple.type
4444
|
4545
| longer explanation available when compiling with `-explain`

tests/neg/i12049.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
-- [E184] Type Error: tests/neg/i12049.scala:14:23 ---------------------------------------------------------------------
1919
14 |val y3: String = ??? : Last[Int *: Int *: Boolean *: String *: EmptyTuple] // error
2020
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21-
| Match type reduction failed since selector EmptyTuple.type
21+
| Match type reduction failed since selector EmptyTuple
2222
| matches none of the cases
2323
|
2424
| case _ *: _ *: t => Last[t]
@@ -48,7 +48,7 @@
4848
-- [E184] Type Error: tests/neg/i12049.scala:25:26 ---------------------------------------------------------------------
4949
25 |val _ = summon[String =:= Last[Int *: Int *: Boolean *: String *: EmptyTuple]] // error
5050
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51-
| Match type reduction failed since selector EmptyTuple.type
51+
| Match type reduction failed since selector EmptyTuple
5252
| matches none of the cases
5353
|
5454
| case _ *: _ *: t => Last[t]

tests/neg/i13780.check

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
| does not uniquely determine parameters a, b in
1515
| case (a, b) => a
1616
| The computed bounds for the parameters are:
17-
| a >: Any
18-
| b >: Any
17+
| a
18+
| b
1919
|
2020
| longer explanation available when compiling with `-explain`
2121
-- [E007] Type Mismatch Error: tests/neg/i13780.scala:18:31 ------------------------------------------------------------
@@ -34,8 +34,8 @@
3434
| does not uniquely determine parameters a, b in
3535
| case (a, b) => a
3636
| The computed bounds for the parameters are:
37-
| a >: Int
38-
| b >: Int
37+
| a <: Int
38+
| b <: Int
3939
|
4040
| longer explanation available when compiling with `-explain`
4141
-- [E007] Type Mismatch Error: tests/neg/i13780.scala:23:37 ------------------------------------------------------------
@@ -54,7 +54,7 @@
5454
| does not uniquely determine parameters a, b in
5555
| case (a, b) => a
5656
| The computed bounds for the parameters are:
57-
| a >: String
58-
| b >: String
57+
| a <: String
58+
| b <: String
5959
|
6060
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)