Skip to content

Commit 5260c60

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 c96d847 commit 5260c60

File tree

8 files changed

+321
-16
lines changed

8 files changed

+321
-16
lines changed

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

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3237,11 +3237,22 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32373237
}
32383238
}
32393239

3240+
def instantiateParamsSpec(insts: Array[Type], caseLambda: HKTypeLambda) = new TypeMap {
3241+
variance = 0
3242+
3243+
def apply(t: Type) = t match {
3244+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
3245+
case t: LazyRef => apply(t.ref)
3246+
case _ => mapOver(t)
3247+
}
3248+
}
3249+
32403250
/** Match a single case. */
32413251
def matchCase(cas: MatchTypeCaseSpec): MatchResult = trace(i"$scrut match ${MatchTypeTrace.caseText(cas)}", matchTypes, show = true) {
32423252
cas match
3243-
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3244-
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
3253+
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3254+
case cas: MatchTypeCaseSpec.SpeccedPatMat => matchSpeccedPatMat(cas)
3255+
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
32453256
}
32463257

32473258
def matchSubTypeTest(spec: MatchTypeCaseSpec.SubTypeTest): MatchResult =
@@ -3253,6 +3264,128 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32533264
MatchResult.Stuck
32543265
end matchSubTypeTest
32553266

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

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

Lines changed: 65 additions & 2 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.*
@@ -5088,8 +5088,23 @@ object Types extends TypeUtils {
50885088
case _ => None
50895089
}
50905090

5091+
enum MatchTypeCasePattern:
5092+
case Capture(num: Int, isWildcard: Boolean)
5093+
case TypeTest(tpe: Type)
5094+
case BaseTypeTest(classType: TypeRef, argPatterns: List[MatchTypeCasePattern], needsConcreteScrut: Boolean)
5095+
5096+
def isTypeTest: Boolean =
5097+
this.isInstanceOf[TypeTest]
5098+
5099+
def needsConcreteScrutInVariantPos: Boolean = this match
5100+
case Capture(_, isWildcard) => !isWildcard
5101+
case TypeTest(_) => false
5102+
case _ => true
5103+
end MatchTypeCasePattern
5104+
50915105
enum MatchTypeCaseSpec:
50925106
case SubTypeTest(origMatchCase: Type, pattern: Type, body: Type)
5107+
case SpeccedPatMat(origMatchCase: HKTypeLambda, captureCount: Int, pattern: MatchTypeCasePattern, body: Type)
50935108
case LegacyPatMat(origMatchCase: HKTypeLambda)
50945109

50955110
def origMatchCase: Type
@@ -5099,11 +5114,59 @@ object Types extends TypeUtils {
50995114
def analyze(cas: Type)(using Context): MatchTypeCaseSpec =
51005115
cas match
51015116
case cas: HKTypeLambda =>
5102-
LegacyPatMat(cas)
5117+
val defn.MatchCase(pat, body) = cas.resultType: @unchecked
5118+
val specPattern = tryConvertToSpecPattern(cas, pat)
5119+
if specPattern != null then
5120+
SpeccedPatMat(cas, cas.paramNames.size, specPattern, body)
5121+
else
5122+
LegacyPatMat(cas)
51035123
case _ =>
51045124
val defn.MatchCase(pat, body) = cas: @unchecked
51055125
SubTypeTest(cas, pat, body)
51065126
end analyze
5127+
5128+
private def tryConvertToSpecPattern(caseLambda: HKTypeLambda, pat: Type)(using Context): MatchTypeCasePattern | Null =
5129+
var typeParamRefsAccountedFor: Int = 0
5130+
5131+
def rec(pat: Type, variance: Int): MatchTypeCasePattern | Null =
5132+
pat match
5133+
case pat @ TypeParamRef(binder, num) if binder eq caseLambda =>
5134+
typeParamRefsAccountedFor += 1
5135+
MatchTypeCasePattern.Capture(num, isWildcard = pat.paramName.is(WildcardParamName))
5136+
5137+
case pat @ AppliedType(tycon: TypeRef, args) if variance == 1 =>
5138+
val tyconSym = tycon.symbol
5139+
if tyconSym.isClass then
5140+
val cls = tyconSym.asClass
5141+
if cls.name.startsWith("Tuple") && defn.isTupleNType(pat) then
5142+
rec(pat.toNestedPairs, variance)
5143+
else
5144+
val tparams = tycon.typeParams
5145+
val argPatterns = args.zip(tparams).map { (arg, tparam) =>
5146+
rec(arg, tparam.paramVarianceSign)
5147+
}
5148+
if argPatterns.exists(_ == null) then
5149+
null
5150+
else
5151+
val argPatterns1 = argPatterns.asInstanceOf[List[MatchTypeCasePattern]] // they are not null
5152+
if argPatterns1.forall(_.isTypeTest) then
5153+
MatchTypeCasePattern.TypeTest(pat)
5154+
else
5155+
val needsConcreteScrut = argPatterns1.zip(tparams).exists {
5156+
(argPattern, tparam) => tparam.paramVarianceSign != 0 && argPattern.needsConcreteScrutInVariantPos
5157+
}
5158+
MatchTypeCasePattern.BaseTypeTest(tycon, argPatterns1, needsConcreteScrut)
5159+
else
5160+
null
5161+
5162+
case _ =>
5163+
MatchTypeCasePattern.TypeTest(pat)
5164+
end rec
5165+
5166+
val result = rec(pat, variance = 1)
5167+
if typeParamRefsAccountedFor == caseLambda.paramNames.size then result
5168+
else null
5169+
end tryConvertToSpecPattern
51075170
end MatchTypeCaseSpec
51085171

51095172
// ------ 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)