Skip to content

Commit 9c76834

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 861b74d commit 9c76834

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

@@ -5058,8 +5058,23 @@ object Types {
50585058
case _ => None
50595059
}
50605060

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

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

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