Skip to content

Commit c61674b

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 a377544 commit c61674b

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
@@ -3221,11 +3221,22 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32213221
}
32223222
}
32233223

3224+
def instantiateParamsSpec(insts: Array[Type], caseLambda: HKTypeLambda) = new TypeMap {
3225+
variance = 0
3226+
3227+
def apply(t: Type) = t match {
3228+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
3229+
case t: LazyRef => apply(t.ref)
3230+
case _ => mapOver(t)
3231+
}
3232+
}
3233+
32243234
/** Match a single case. */
32253235
def matchCase(cas: MatchTypeCaseSpec): MatchResult = trace(i"$scrut match ${MatchTypeTrace.caseText(cas)}", matchTypes, show = true) {
32263236
cas match
3227-
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3228-
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
3237+
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3238+
case cas: MatchTypeCaseSpec.SpeccedPatMat => matchSpeccedPatMat(cas)
3239+
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
32293240
}
32303241

32313242
def matchSubTypeTest(spec: MatchTypeCaseSpec.SubTypeTest): MatchResult =
@@ -3237,6 +3248,128 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32373248
MatchResult.Stuck
32383249
end matchSubTypeTest
32393250

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