Skip to content

Commit 881e945

Browse files
authored
SIP-56: Better foundations for match types (#18262)
Implementation of SIP-56: Proper Specification for Match Types https://docs.scala-lang.org/sips/match-types-spec.html
2 parents 8a3fc7a + c3b9d9b commit 881e945

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1670
-290
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/MatchTypeTrace.scala

+14-6
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ object MatchTypeTrace:
1212

1313
private enum TraceEntry:
1414
case TryReduce(scrut: Type)
15-
case Stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])
16-
case NoInstance(scrut: Type, stuckCase: Type, fails: List[(Name, TypeBounds)])
15+
case Stuck(scrut: Type, stuckCase: MatchTypeCaseSpec, otherCases: List[MatchTypeCaseSpec])
16+
case NoInstance(scrut: Type, stuckCase: MatchTypeCaseSpec, fails: List[(Name, TypeBounds)])
1717
case EmptyScrutinee(scrut: Type)
1818
import TraceEntry.*
1919

@@ -54,10 +54,10 @@ object MatchTypeTrace:
5454
* not disjoint from it either, which means that the remaining cases `otherCases`
5555
* cannot be visited. Only the first failure is recorded.
5656
*/
57-
def stuck(scrut: Type, stuckCase: Type, otherCases: List[Type])(using Context) =
57+
def stuck(scrut: Type, stuckCase: MatchTypeCaseSpec, otherCases: List[MatchTypeCaseSpec])(using Context) =
5858
matchTypeFail(Stuck(scrut, stuckCase, otherCases))
5959

60-
def noInstance(scrut: Type, stuckCase: Type, fails: List[(Name, TypeBounds)])(using Context) =
60+
def noInstance(scrut: Type, stuckCase: MatchTypeCaseSpec, fails: List[(Name, TypeBounds)])(using Context) =
6161
matchTypeFail(NoInstance(scrut, stuckCase, fails))
6262

6363
/** Record a failure that scrutinee `scrut` is provably empty.
@@ -80,13 +80,16 @@ object MatchTypeTrace:
8080
case _ =>
8181
op
8282

83+
def caseText(spec: MatchTypeCaseSpec)(using Context): String =
84+
caseText(spec.origMatchCase)
85+
8386
def caseText(tp: Type)(using Context): String = tp match
8487
case tp: HKTypeLambda => caseText(tp.resultType)
8588
case defn.MatchCase(any, body) if any eq defn.AnyType => i"case _ => $body"
8689
case defn.MatchCase(pat, body) => i"case $pat => $body"
8790
case _ => i"case $tp"
8891

89-
private def casesText(cases: List[Type])(using Context) =
92+
private def casesText(cases: List[MatchTypeCaseSpec])(using Context) =
9093
i"${cases.map(caseText)}%\n %"
9194

9295
private def explainEntry(entry: TraceEntry)(using Context): String = entry match
@@ -116,10 +119,15 @@ object MatchTypeTrace:
116119
| ${fails.map((name, bounds) => i"$name$bounds")}%\n %"""
117120

118121
/** The failure message when the scrutinee `scrut` does not match any case in `cases`. */
119-
def noMatchesText(scrut: Type, cases: List[Type])(using Context): String =
122+
def noMatchesText(scrut: Type, cases: List[MatchTypeCaseSpec])(using Context): String =
120123
i"""failed since selector $scrut
121124
|matches none of the cases
122125
|
123126
| ${casesText(cases)}"""
124127

128+
def illegalPatternText(scrut: Type, cas: MatchTypeCaseSpec.LegacyPatMat)(using Context): String =
129+
i"""The match type contains an illegal case:
130+
| ${caseText(cas)}
131+
|(this error can be ignored for now with `-source:3.3`)"""
132+
125133
end MatchTypeTrace

Diff for: compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+493-216
Large diffs are not rendered by default.

Diff for: compiler/src/dotty/tools/dotc/core/Types.scala

+188-2
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.*
@@ -30,6 +30,8 @@ import Hashable.*
3030
import Uniques.*
3131
import collection.mutable
3232
import config.Config
33+
import config.Feature.sourceVersion
34+
import config.SourceVersion
3335
import annotation.{tailrec, constructorOnly}
3436
import scala.util.hashing.{ MurmurHash3 => hashing }
3537
import config.Printers.{core, typr, matchTypes}
@@ -5036,7 +5038,7 @@ object Types extends TypeUtils {
50365038
trace(i"reduce match type $this $hashCode", matchTypes, show = true)(inMode(Mode.Type) {
50375039
def matchCases(cmp: TrackingTypeComparer): Type =
50385040
val saved = ctx.typerState.snapshot()
5039-
try cmp.matchCases(scrutinee.normalized, cases)
5041+
try cmp.matchCases(scrutinee.normalized, cases.map(MatchTypeCaseSpec.analyze(_)))
50405042
catch case ex: Throwable =>
50415043
handleRecursive("reduce type ", i"$scrutinee match ...", ex)
50425044
finally
@@ -5088,6 +5090,190 @@ object Types extends TypeUtils {
50885090
case _ => None
50895091
}
50905092

5093+
enum MatchTypeCasePattern:
5094+
case Capture(num: Int, isWildcard: Boolean)
5095+
case TypeTest(tpe: Type)
5096+
case BaseTypeTest(classType: TypeRef, argPatterns: List[MatchTypeCasePattern], needsConcreteScrut: Boolean)
5097+
case CompileTimeS(argPattern: MatchTypeCasePattern)
5098+
case AbstractTypeConstructor(tycon: Type, argPatterns: List[MatchTypeCasePattern])
5099+
case TypeMemberExtractor(typeMemberName: TypeName, capture: Capture)
5100+
5101+
def isTypeTest: Boolean =
5102+
this.isInstanceOf[TypeTest]
5103+
5104+
def needsConcreteScrutInVariantPos: Boolean = this match
5105+
case Capture(_, isWildcard) => !isWildcard
5106+
case TypeTest(_) => false
5107+
case _ => true
5108+
end MatchTypeCasePattern
5109+
5110+
enum MatchTypeCaseSpec:
5111+
case SubTypeTest(origMatchCase: Type, pattern: Type, body: Type)
5112+
case SpeccedPatMat(origMatchCase: HKTypeLambda, captureCount: Int, pattern: MatchTypeCasePattern, body: Type)
5113+
case LegacyPatMat(origMatchCase: HKTypeLambda)
5114+
case MissingCaptures(origMatchCase: HKTypeLambda, missing: collection.BitSet)
5115+
5116+
def origMatchCase: Type
5117+
end MatchTypeCaseSpec
5118+
5119+
object MatchTypeCaseSpec:
5120+
def analyze(cas: Type)(using Context): MatchTypeCaseSpec =
5121+
cas match
5122+
case cas: HKTypeLambda if !sourceVersion.isAtLeast(SourceVersion.`3.4`) =>
5123+
// Always apply the legacy algorithm under -source:3.3 and below
5124+
LegacyPatMat(cas)
5125+
case cas: HKTypeLambda =>
5126+
val defn.MatchCase(pat, body) = cas.resultType: @unchecked
5127+
val missing = checkCapturesPresent(cas, pat)
5128+
if !missing.isEmpty then
5129+
MissingCaptures(cas, missing)
5130+
else
5131+
val specPattern = tryConvertToSpecPattern(cas, pat)
5132+
if specPattern != null then
5133+
SpeccedPatMat(cas, cas.paramNames.size, specPattern, body)
5134+
else
5135+
LegacyPatMat(cas)
5136+
case _ =>
5137+
val defn.MatchCase(pat, body) = cas: @unchecked
5138+
SubTypeTest(cas, pat, body)
5139+
end analyze
5140+
5141+
/** Checks that all the captures of the case are present in the case.
5142+
*
5143+
* Sometimes, because of earlier substitutions of an abstract type constructor,
5144+
* we can end up with patterns that do not mention all their captures anymore.
5145+
* This can happen even when the body still refers to these missing captures.
5146+
* In that case, we must always consider the case to be unmatchable, i.e., to
5147+
* become `Stuck`.
5148+
*
5149+
* See pos/i12127.scala for an example.
5150+
*/
5151+
def checkCapturesPresent(cas: HKTypeLambda, pat: Type)(using Context): collection.BitSet =
5152+
val captureCount = cas.paramNames.size
5153+
val missing = new mutable.BitSet(captureCount)
5154+
missing ++= (0 until captureCount)
5155+
new CheckCapturesPresent(cas).apply(missing, pat)
5156+
5157+
private class CheckCapturesPresent(cas: HKTypeLambda)(using Context) extends TypeAccumulator[mutable.BitSet]:
5158+
def apply(missing: mutable.BitSet, tp: Type): mutable.BitSet = tp match
5159+
case TypeParamRef(binder, num) if binder eq cas =>
5160+
missing -= num
5161+
case _ =>
5162+
foldOver(missing, tp)
5163+
end CheckCapturesPresent
5164+
5165+
/** Tries to convert a match type case pattern in HKTypeLambda form into a spec'ed `MatchTypeCasePattern`.
5166+
*
5167+
* This method recovers the structure of *legal patterns* as defined in SIP-56
5168+
* from the unstructured `HKTypeLambda` coming from the typer.
5169+
*
5170+
* It must adhere to the specification of legal patterns defined at
5171+
* https://docs.scala-lang.org/sips/match-types-spec.html#legal-patterns
5172+
*
5173+
* Returns `null` if the pattern in `caseLambda` is a not a legal pattern.
5174+
*/
5175+
private def tryConvertToSpecPattern(caseLambda: HKTypeLambda, pat: Type)(using Context): MatchTypeCasePattern | Null =
5176+
var typeParamRefsAccountedFor: Int = 0
5177+
5178+
def rec(pat: Type, variance: Int): MatchTypeCasePattern | Null =
5179+
pat match
5180+
case pat @ TypeParamRef(binder, num) if binder eq caseLambda =>
5181+
typeParamRefsAccountedFor += 1
5182+
MatchTypeCasePattern.Capture(num, isWildcard = pat.paramName.is(WildcardParamName))
5183+
5184+
case pat @ AppliedType(tycon: TypeRef, args) if variance == 1 =>
5185+
val tyconSym = tycon.symbol
5186+
if tyconSym.isClass then
5187+
if tyconSym.name.startsWith("Tuple") && defn.isTupleNType(pat) then
5188+
rec(pat.toNestedPairs, variance)
5189+
else
5190+
recArgPatterns(pat) { argPatterns =>
5191+
val needsConcreteScrut = argPatterns.zip(tycon.typeParams).exists {
5192+
(argPattern, tparam) => tparam.paramVarianceSign != 0 && argPattern.needsConcreteScrutInVariantPos
5193+
}
5194+
MatchTypeCasePattern.BaseTypeTest(tycon, argPatterns, needsConcreteScrut)
5195+
}
5196+
else if defn.isCompiletime_S(tyconSym) && args.sizeIs == 1 then
5197+
val argPattern = rec(args.head, variance)
5198+
if argPattern == null then
5199+
null
5200+
else if argPattern.isTypeTest then
5201+
MatchTypeCasePattern.TypeTest(pat)
5202+
else
5203+
MatchTypeCasePattern.CompileTimeS(argPattern)
5204+
else
5205+
tycon.info match
5206+
case _: RealTypeBounds =>
5207+
recAbstractTypeConstructor(pat)
5208+
case TypeAlias(tl @ HKTypeLambda(onlyParam :: Nil, resType: RefinedType)) =>
5209+
/* Unlike for eta-expanded classes, the typer does not automatically
5210+
* dealias poly type aliases to refined types. So we have to give them
5211+
* a chance here.
5212+
* We are quite specific about the shape of type aliases that we are willing
5213+
* to dealias this way, because we must not dealias arbitrary type constructors
5214+
* that could refine the bounds of the captures; those would amount of
5215+
* type-test + capture combos, which are out of the specced match types.
5216+
*/
5217+
rec(pat.superType, variance)
5218+
case _ =>
5219+
null
5220+
5221+
case pat @ AppliedType(tycon: TypeParamRef, _) if variance == 1 =>
5222+
recAbstractTypeConstructor(pat)
5223+
5224+
case pat @ RefinedType(parent, refinedName: TypeName, TypeAlias(alias @ TypeParamRef(binder, num)))
5225+
if variance == 1 && (binder eq caseLambda) =>
5226+
parent.member(refinedName) match
5227+
case refinedMember: SingleDenotation if refinedMember.exists =>
5228+
// Check that the bounds of the capture contain the bounds of the inherited member
5229+
val refinedMemberBounds = refinedMember.info
5230+
val captureBounds = caseLambda.paramInfos(num)
5231+
if captureBounds.contains(refinedMemberBounds) then
5232+
/* In this case, we know that any member we eventually find during reduction
5233+
* will have bounds that fit in the bounds of the capture. Therefore, no
5234+
* type-test + capture combo is necessary, and we can apply the specced match types.
5235+
*/
5236+
val capture = rec(alias, variance = 0).asInstanceOf[MatchTypeCasePattern.Capture]
5237+
MatchTypeCasePattern.TypeMemberExtractor(refinedName, capture)
5238+
else
5239+
// Otherwise, a type-test + capture combo might be necessary, and we are out of spec
5240+
null
5241+
case _ =>
5242+
// If the member does not refine a member of the `parent`, we are out of spec
5243+
null
5244+
5245+
case _ =>
5246+
MatchTypeCasePattern.TypeTest(pat)
5247+
end rec
5248+
5249+
def recAbstractTypeConstructor(pat: AppliedType): MatchTypeCasePattern | Null =
5250+
recArgPatterns(pat) { argPatterns =>
5251+
MatchTypeCasePattern.AbstractTypeConstructor(pat.tycon, argPatterns)
5252+
}
5253+
end recAbstractTypeConstructor
5254+
5255+
def recArgPatterns(pat: AppliedType)(whenNotTypeTest: List[MatchTypeCasePattern] => MatchTypeCasePattern | Null): MatchTypeCasePattern | Null =
5256+
val AppliedType(tycon, args) = pat
5257+
val tparams = tycon.typeParams
5258+
val argPatterns = args.zip(tparams).map { (arg, tparam) =>
5259+
rec(arg, tparam.paramVarianceSign)
5260+
}
5261+
if argPatterns.exists(_ == null) then
5262+
null
5263+
else
5264+
val argPatterns1 = argPatterns.asInstanceOf[List[MatchTypeCasePattern]] // they are not null
5265+
if argPatterns1.forall(_.isTypeTest) then
5266+
MatchTypeCasePattern.TypeTest(pat)
5267+
else
5268+
whenNotTypeTest(argPatterns1)
5269+
end recArgPatterns
5270+
5271+
val result = rec(pat, variance = 1)
5272+
if typeParamRefsAccountedFor == caseLambda.paramNames.size then result
5273+
else null
5274+
end tryConvertToSpecPattern
5275+
end MatchTypeCaseSpec
5276+
50915277
// ------ ClassInfo, Type Bounds --------------------------------------------------
50925278

50935279
type TypeOrSymbol = Type | Symbol

Diff for: compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

+1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
204204
case VarArgsParamCannotBeGivenID // errorNumber: 188
205205
case ExtractorNotFoundID // errorNumber: 189
206206
case PureUnitExpressionID // errorNumber: 190
207+
case MatchTypeLegacyPatternID // errorNumber: 191
207208

208209
def errorNumber = ordinal - 1
209210

Diff for: compiler/src/dotty/tools/dotc/reporting/messages.scala

+4
Original file line numberDiff line numberDiff line change
@@ -3073,6 +3073,10 @@ class MatchTypeScrutineeCannotBeHigherKinded(tp: Type)(using Context)
30733073
def msg(using Context) = i"the scrutinee of a match type cannot be higher-kinded"
30743074
def explain(using Context) = ""
30753075

3076+
class MatchTypeLegacyPattern(errorText: String)(using Context) extends TypeMsg(MatchTypeLegacyPatternID):
3077+
def msg(using Context) = errorText
3078+
def explain(using Context) = ""
3079+
30763080
class ClosureCannotHaveInternalParameterDependencies(mt: Type)(using Context)
30773081
extends TypeMsg(ClosureCannotHaveInternalParameterDependenciesID):
30783082
def msg(using Context) =

Diff for: compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ object TypeTestsCasts {
154154

155155
case x =>
156156
// always false test warnings are emitted elsewhere
157-
TypeComparer.provablyDisjoint(x, tpe.derivedAppliedType(tycon, targs.map(_ => WildcardType)))
157+
// provablyDisjoint wants fully applied types as input; because we're in the middle of erasure, we sometimes get raw types here
158+
val xApplied =
159+
val tparams = x.typeParams
160+
if tparams.isEmpty then x else x.appliedTo(tparams.map(_ => WildcardType))
161+
TypeComparer.provablyDisjoint(xApplied, tpe.derivedAppliedType(tycon, targs.map(_ => WildcardType)))
158162
|| typeArgsDeterminable(X, tpe)
159163
||| i"its type arguments can't be determined from $X"
160164
}

Diff for: compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ trait TypeAssigner {
297297
else fntpe.resultType // fast path optimization
298298
else
299299
errorType(em"wrong number of arguments at ${ctx.phase.prev} for $fntpe: ${fn.tpe}, expected: ${fntpe.paramInfos.length}, found: ${args.length}", tree.srcPos)
300+
case err: ErrorType =>
301+
err
300302
case t =>
301303
if (ctx.settings.Ydebug.value) new FatalError("").printStackTrace()
302304
errorType(err.takesNoParamsMsg(fn, ""), tree.srcPos)
@@ -563,5 +565,3 @@ object TypeAssigner extends TypeAssigner:
563565
def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match
564566
case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType)
565567
case _ => if ctx.erasedTypes then defn.SeqType else defn.SeqType.appliedTo(elemType)
566-
567-

Diff for: tests/neg/12800.scala

-21
This file was deleted.

Diff for: tests/neg/6314-1.scala

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
object G {
2-
final class X
3-
final class Y
2+
trait X
3+
class Y
4+
class Z
45

56
trait FooSig {
67
type Type
@@ -13,14 +14,14 @@ object G {
1314
type Foo = Foo.Type
1415

1516
type Bar[A] = A match {
16-
case X & Y => String
17+
case X & Z => String
1718
case Y => Int
1819
}
1920

2021
def main(args: Array[String]): Unit = {
2122
val a: Bar[X & Y] = "hello" // error
2223
val i: Bar[Y & Foo] = Foo.apply[Bar](a)
23-
val b: Int = i // error
24+
val b: Int = i
2425
println(b + 1)
2526
}
2627
}

Diff for: tests/neg/6314-6.check

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Error: tests/neg/6314-6.scala:26:3 ----------------------------------------------------------------------------------
2+
26 | (new YY {}).boom // error: object creation impossible
3+
| ^
4+
|object creation impossible, since def apply(fa: String): Int in trait XX in object Test3 is not defined
5+
|(Note that
6+
| parameter String in def apply(fa: String): Int in trait XX in object Test3 does not match
7+
| parameter Test3.Bar[X & Object with Test3.YY {...}#Foo] in def apply(fa: Test3.Bar[X & YY.this.Foo]): Test3.Bar[Y & YY.this.Foo] in trait YY in object Test3
8+
| )
9+
-- Error: tests/neg/6314-6.scala:52:3 ----------------------------------------------------------------------------------
10+
52 | (new YY {}).boom // error: object creation impossible
11+
| ^
12+
|object creation impossible, since def apply(fa: String): Int in trait XX in object Test4 is not defined
13+
|(Note that
14+
| parameter String in def apply(fa: String): Int in trait XX in object Test4 does not match
15+
| parameter Test4.Bar[X & Object with Test4.YY {...}#FooAlias] in def apply(fa: Test4.Bar[X & YY.this.FooAlias]): Test4.Bar[Y & YY.this.FooAlias] in trait YY in object Test4
16+
| )

0 commit comments

Comments
 (0)