Skip to content

Commit d0174ff

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 37fc96a commit d0174ff

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
@@ -3225,11 +3225,22 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32253225
}
32263226
}
32273227

3228+
def instantiateParamsSpec(insts: Array[Type], caseLambda: HKTypeLambda) = new TypeMap {
3229+
variance = 0
3230+
3231+
def apply(t: Type) = t match {
3232+
case t @ TypeParamRef(b, n) if b `eq` caseLambda => insts(n)
3233+
case t: LazyRef => apply(t.ref)
3234+
case _ => mapOver(t)
3235+
}
3236+
}
3237+
32283238
/** Match a single case. */
32293239
def matchCase(cas: MatchTypeCaseSpec): MatchResult = trace(i"$scrut match ${MatchTypeTrace.caseText(cas)}", matchTypes, show = true) {
32303240
cas match
3231-
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3232-
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
3241+
case cas: MatchTypeCaseSpec.SubTypeTest => matchSubTypeTest(cas)
3242+
case cas: MatchTypeCaseSpec.SpeccedPatMat => matchSpeccedPatMat(cas)
3243+
case cas: MatchTypeCaseSpec.LegacyPatMat => matchLegacyPatMat(cas)
32333244
}
32343245

32353246
def matchSubTypeTest(spec: MatchTypeCaseSpec.SubTypeTest): MatchResult =
@@ -3241,6 +3252,128 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32413252
MatchResult.Stuck
32423253
end matchSubTypeTest
32433254

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

@@ -5061,8 +5061,23 @@ object Types {
50615061
case _ => None
50625062
}
50635063

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

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

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