Skip to content

Commit 3a2c49e

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 d392300 commit 3a2c49e

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
@@ -3229,11 +3229,22 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32293229
}
32303230
}
32313231

3232+
def instantiateParamsSpec(insts: Array[Type], caseLambda: HKTypeLambda) = new TypeMap {
3233+
variance = 0
3234+
3235+
def apply(t: Type) = t match {
3236+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
3237+
case t: LazyRef => apply(t.ref)
3238+
case _ => mapOver(t)
3239+
}
3240+
}
3241+
32323242
/** Match a single case. */
32333243
def matchCase(cas: MatchTypeCaseSpec): MatchResult = trace(i"$scrut match ${MatchTypeTrace.caseText(cas)}", matchTypes, show = true) {
32343244
cas match
3235-
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3236-
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
3245+
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3246+
case cas: MatchTypeCaseSpec.SpeccedPatMat => matchSpeccedPatMat(cas)
3247+
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
32373248
}
32383249

32393250
def matchSubTypeTest(spec: MatchTypeCaseSpec.SubTypeTest): MatchResult =
@@ -3245,6 +3256,128 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32453256
MatchResult.Stuck
32463257
end matchSubTypeTest
32473258

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

@@ -5089,8 +5089,23 @@ object Types {
50895089
case _ => None
50905090
}
50915091

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

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

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